首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau
百百 BLOG
首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau

Kurau Blog

「隨心而寫,真真假假,都是我」

一個記錄生活、輸出興趣的個人空間。
遊戲、影視、閱讀、學習……每一段體驗都值得留下文字。

頁面導覽

  • 學習紀錄
  • 遊戲心得
  • 影視Life
  • 書單
  • 委託作品與二創
  • Kurau
  • 合作邀請

找到我

歡迎來 Discord 找我聊天!

“曾經發生的事不可能忘記,只是暫時想不起來而已。”-《神隱少女》

© 2026 Kurau All rights reserved

前端框架

index key 請使用 uuid

By Kurau·2023-02-04·Updated 2026-05-09·4 分鐘閱讀

React list key 不要用 index,改用 UUID

TL;DR
key={index} 在「列表會新增/刪除/排序」時會讓 React reconciler 認錯元件,造成 state 錯置、不必要的 re-render、輸入框內容跑掉等難解 bug。列表元素有穩定 ID 就用它,沒有就生 UUID。純靜態列表(從不變動)才能放心用 index。

為什麼 index 不行

React 用 key 判斷「這個元素是同一個還是不同個」。如果用 index:

// 列表:[Alice, Bob, Charlie]
<li key={0}>Alice</li>     // index=0
<li key={1}>Bob</li>       // index=1
<li key={2}>Charlie</li>   // index=2
tsx

刪掉 Alice 後:

// 列表:[Bob, Charlie]
<li key={0}>Bob</li>       // ⚠️ React 認為 key=0 還是同一個元素,只是內容變 Bob
<li key={1}>Charlie</li>   // ⚠️ 同理
tsx

React 不會 unmount Alice 那個 component,只會把它的內容改成 Bob。任何內部 state(輸入框值、focus、動畫進度)會留在錯的位置。


真實 bug 範例

const [items, setItems] = useState(['A', 'B', 'C']);

return items.map((item, i) => (
  <li key={i}>
    {item}
    <input defaultValue={item} />  {/* ⚠️ defaultValue 只在 mount 時讀 */}
  </li>
));
tsx

使用者在 B 的 input 改成 "BBB" → 點刪除 A → B 的 input 仍顯示 "BBB",但對應的 item 已經變成 C。input 的 state 跟資料對不上。

改用 UUID:

const [items, setItems] = useState([
  { id: 'a-uuid', text: 'A' },
  { id: 'b-uuid', text: 'B' },
  { id: 'c-uuid', text: 'C' },
]);

return items.map((item) => (
  <li key={item.id}>      {/* ✅ 穩定 key */}
    {item.text}
    <input defaultValue={item.text} />
  </li>
));
tsx

刪除 A → React 正確 unmount A 那個 li → B 的 input 跟著 B 走。


何時 index 是 OK 的

安全條件(三個都滿足)
  1. 列表 永遠不變(資料 push 後不會排序、刪除、insert)
  2. 列表 沒有 component 內部 state(沒 input、沒動畫、沒 focus 追蹤)
  3. 不需要避開不必要的 re-render

例:渲染一份 靜態的 nav 連結清單、bullet point list,用 index 沒問題。


實際範例:用 react-uuid

npm install react-uuid
bash
import uuid from 'react-uuid';
import { Fragment } from 'react';

function LearningContents({ contents }: { contents: any[] }) {
  if (!contents) return null;

  return (
    <div className="collection_article">
      {contents.sort().map((part: { [name: string]: string }) => (
        <div className="learning_article_part" key={uuid()}>
          {Object.keys(part).sort().map((keyName) => (
            <Fragment key={uuid()}>
              {createArticleElement(keyName, part[keyName])}
            </Fragment>
          ))}
        </div>
      ))}
    </div>
  );
}
tsx
key={uuid()} 是反 pattern!
上面範例每次 render 都會生新 UUID,等於每次 React 都認為 所有元素都是新的 → 整個列表 re-mount,效能爛、輸入框會被打掉。

正確做法:UUID 應該在 資料建立時 就生好,跟資料一起存:

const newItem = { id: uuid(), text: 'new' };  // ✅ id 在建立時生
setItems([...items, newItem]);

// render 時:
{items.map((item) => <li key={item.id}>...</li>)}
typescript

套件選擇

套件大小API
crypto.randomUUID()(原生)0 bytescrypto.randomUUID()
nanoid130 bytesnanoid() → 21 字元短 ID
uuid(uuid v4)1.5KBimport { v4 } from 'uuid'; v4()
react-uuid較重uuid()(就是這篇用的)

2026 年首選 crypto.randomUUID()(所有現代瀏覽器內建)或 nanoid(更短、URL-safe)。

const id = crypto.randomUUID();
// '550e8400-e29b-41d4-a716-446655440000'
typescript

進階:從現有資料推導穩定 key

如果資料本身有唯一識別(資料庫 id、URL slug、檔名),直接用,別生 UUID:

// ✅ 用資料庫 id
{users.map(user => <UserCard key={user.id} {...user} />)}

// ✅ 用 URL slug
{posts.map(post => <PostLink key={post.slug} {...post} />)}

// ✅ 用組合 key
{matches.map(m => <Row key={`${m.year}-${m.team}`} {...m} />)}
tsx

目錄

    ◆ 相關文章

    • HTML React 打字機效果

      2026-05-09
    • Framer Motion 打造比最棒還要棒的動畫

      2026-05-09
    • Redux

      2026-05-09
    • react-app-env.d.ts

      2026-05-09
    ← 上一篇Dynamic Programming下一篇 →Firebase 資料庫如何建立 與 上傳

    ◆ 關於作者

    Kurau

    個人寫作 / 創作的 SoT,記錄遊戲、影視、學習與生活。

    更多 Kurau 的文章