在開發 React 或 Next.js 專案時,若使用 .map() 迴圈渲染陣列資料,有時會在瀏覽器開發者工具看到紅字警告:Warning: Encountered two children with the same key。這則訊息代表在同一層級的子元件中出現了重複的 key 屬性值,進而影響 React Virtual DOM 的比對(Reconciliation)機制。本篇整理這個警告的觸發原因、常見的 index 誤用陷阱,以及在實務專案中產生唯一 key 的解法。
一、Console Warning 完整警告訊息與觸發原因
當 React 在同一個父容器底下偵測到兩個以上子元素擁有相同的 key 時,Console 會印出以下警告:
Warning: Encountered two children with the same key, `1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
為什麼 React 要求 key 必須唯一?
在 React 的渲染機制中,key 是用來追蹤列表中每個元素身分的唯一識別碼。當陣列資料發生新增、刪除或重新排序時,React 會依賴 key 來決定哪些 DOM 節點可以複用、哪些需要重新建立。
如果陣列中有兩筆資料的 key 重複(例如都是 1 或都是 undefined),React 在更新畫面時可能會:
- 漏渲染或重複渲染子元件:刪除其中一筆資料時,畫面上卻刪錯成另一筆。
- 元件內部 State 錯亂:輸入框(
<input>)或勾選狀態綁定在錯誤的項目上。
以下是最常見的錯誤觸發範例(陣列中的 id 本身重複):
const data = [
{ id: 1, name: '項目 A' },
{ id: 2, name: '項目 B' },
{ id: 1, name: '項目 C' }, // id: 1 與第一筆重複!
{ id: 2, name: '項目 D' }, // id: 2 與第二筆重複!
];
export default function ItemList() {
return (
<ul>
{data.map((item) => (
// 觸發 Warning: Encountered two children with the same key, `1`
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
二、解決重複 Key 的 3 種實務做法
遇到 Encountered Two Children with the Same Key 時,應先確認資料來源為何會重複,再依資料性質選擇對應的處理方式:
解法 1:組合多個欄位建立複合唯一 Key(Composite Key)
如果後端 API 回傳的清單是由不同來源合併而成(例如同時包含「商品 id: 1」與「文章 id: 1」,或是同一個 id 在不同名字下出現),可以將多個具有區別性的欄位字串拼接起來作為複合 key:
const data = [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 1, name: 'c' },
{ id: 2, name: 'd' },
];
export default function ItemList() {
return (
<ul>
{data.map((item) => (
// 結合 id 與 name 組合成唯一識別字串(如 "1-a"、"1-c")
<li key={`${item.id}-${item.name}`}>{item.name}</li>
))}
</ul>
);
}
解法 2:在寫入 State 前先用 crypto.randomUUID() 補齊唯一 ID
如果原始資料完全沒有唯一欄位,建議在接收到 API 資料或新增項目寫入 State 的當下,先透過瀏覽器原生的 crypto.randomUUID() 為每筆資料生成穩定的 _uid,切記不要在 JSX 的 key={crypto.randomUUID()} 內直接呼叫(否則每次渲染都會產生新 Key 導致元件不斷卸載重掛):
import { useState } from 'react';
// 在資料初始化或寫入 State 時預先產生唯一 _uid
const initialData = [
{ id: 1, name: 'a' },
{ id: 1, name: 'c' },
].map((item) => ({
...item,
_uid: crypto.randomUUID(),
}));
export default function SafeList() {
const [items] = useState(initialData);
return (
<ul>
{items.map((item) => (
<li key={item._uid}>{item.name}</li>
))}
</ul>
);
}
解法 3:過濾滾動載入(Infinite Scroll)造成的重複資料
在實作無限滾動或分頁載入時,常因為非同步請求重複發送,導致同一頁的資料被 concat 塞進陣列兩次。此時應在更新 State 時使用 Map 針對 id 進行去重:
// 使用 Map 根據 item.id 自動剔除重複資料
const uniqueData = Array.from(
new Map(rawList.map((item) => [item.id, item])).values()
);
三、常見誤區:可以直接改用陣列 index 當作 key 嗎?
網路上許多文章會建議直接把 .map((item, index) => <Child key={index} />) 當成萬用解藥。雖然傳入 index(0, 1, 2, 3...)確實能讓 Console 警告立刻消失,但必須清楚區分適用邊界:
// 僅限「純靜態展示、不會排序或刪減」的列表才可使用 index
{staticData.map((item, index) => (
<ChildComponent key={index} data={item} />
))}
| 列表資料特性 | 是否可用 key={index} | 原因與潛在影響 |
|---|---|---|
| 純靜態顯示清單(無新增、刪除、排序,且無內部 State) | 可以 | 陣列順序固定不變,索引值與項目對應關係不會錯位 |
| 動態列表(可刪除項目、拖曳排序、篩選或頂部插入) | 不建議 | 刪除第 0 項後,原本第 1 項的 index 會變成 0,導致 React 誤認並保留舊狀態 |
每次 Render 動態亂數(如 key={Math.random()}) | 絕對禁止 | 每次重新渲染都會產生全新 Key,導致所有子元件 DOM 被強制銷毀重建,效能嚴重劣化 |
💡 延伸閱讀:處理完列表渲染的
key警告後,若在列表項目中綁定點擊或刪除按鈕時遇到事件傳參問題,可接續參考 React 事件處理器 (Event Handlers):常用事件速查表與傳參寫法。