index key 請使用 uuid
React list key 不要用 index,改用 UUID
TL;DRkey={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
刪掉 Alice 後:
// 列表:[Bob, Charlie]
<li key={0}>Bob</li> // ⚠️ React 認為 key=0 還是同一個元素,只是內容變 Bob
<li key={1}>Charlie</li> // ⚠️ 同理
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>
));
使用者在 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>
));
刪除 A → React 正確 unmount A 那個 li → B 的 input 跟著 B 走。
何時 index 是 OK 的
安全條件(三個都滿足)
- 列表 永遠不變(資料 push 後不會排序、刪除、insert)
- 列表 沒有 component 內部 state(沒 input、沒動畫、沒 focus 追蹤)
- 不需要避開不必要的 re-render
例:渲染一份 靜態的 nav 連結清單、bullet point list,用 index 沒問題。
實際範例:用 react-uuid
npm install react-uuid
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>
);
}
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>)}
套件選擇
| 套件 | 大小 | API |
|---|---|---|
crypto.randomUUID()(原生) | 0 bytes | crypto.randomUUID() |
nanoid | 130 bytes | nanoid() → 21 字元短 ID |
uuid(uuid v4) | 1.5KB | import { v4 } from 'uuid'; v4() |
react-uuid | 較重 | uuid()(就是這篇用的) |
2026 年首選 crypto.randomUUID()(所有現代瀏覽器內建)或 nanoid(更短、URL-safe)。
const id = crypto.randomUUID();
// '550e8400-e29b-41d4-a716-446655440000'
進階:從現有資料推導穩定 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} />)}