上一篇講到 React State 更新時,我們會盡量建立新的 Array / Object。
但只要開始 Render List,很快又會看到另一個幾乎無所不在的東西:
key
例如:
items.map(item => (
<div key={item.id}>
{item.name}
</div>
))
第一次看到時,我很容易把 key 理解成:
React 規定列表一定要放的東西。
但真正重要的不是「有沒有寫 key」。
而是:
React 到底要怎麼知道這一筆資料是誰?
key 的列表開始假設:
const items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
];
Render:
items.map(item => (
<div>
{item.name}
</div>
))
畫面:
Apple
Banana
Orange
看起來完全沒問題。
但 React 會需要知道:
上一輪 Render 的 Apple
↓
是不是這一輪的 Apple?
上一輪的 Banana
↓
是不是這一輪的 Banana?
因為列表不一定永遠不變。
可能會:
新增
刪除
排序
更新
所以 React 需要一個可以辨認每一筆資料的方式。
這就是:
key
key 可以先理解成「這一筆資料的身分證」例如:
items.map(item => (
<div key={item.id}>
{item.name}
</div>
))
這裡:
key={item.id}
等於告訴 React:
id = 1
→ 這是同一筆 Apple
id = 2
→ 這是同一筆 Banana
id = 3
→ 這是同一筆 Orange
所以就算順序改了:
Orange
Apple
Banana
React 還是可以透過:
1
2
3
知道誰是誰。
index?例如:
items.map((item, index) => (
<div key={index}>
{item.name}
</div>
))
這樣通常不會馬上報錯。
而且畫面也看起來正常。
所以初學時很容易覺得:
那全部 key={index} 不就好了?
問題在於:
index 代表的是位置
不是:
這筆資料真正的身份
原本:
index 0 → Apple
index 1 → Banana
index 2 → Orange
如果刪掉 Apple:
Banana
Orange
新的 index 變成:
index 0 → Banana
index 1 → Orange
也就是:
原本 index 0 是 Apple
現在 index 0 變 Banana
原本 index 1 是 Banana
現在 index 1 變 Orange
如果 React 只看 index:
0
1
它可能會認為:
原本第 0 個還是第 0 個。
但實際資料已經不是同一筆了。
如果列表只是:
Apple
Banana
Orange
可能很難看出問題。
但如果每一列裡面還有自己的 UI 狀態,例如:
<input />
或:
checkbox
expanded state
editing state
animation
就可能出現很奇怪的結果。
例如:
Apple 的 input
Banana 的 input
Orange 的 input
使用者正在編輯 Banana。
這時候刪掉 Apple。
如果用 index 當 key,
React 可能會把原本某個位置的 UI 狀態重用到另一筆資料上。
最後就可能看到:
「我明明編輯 Banana,為什麼刪掉上一筆後狀態跑掉了?」
所以比較理想的是:
items.map(item => (
<div key={item.id}>
{item.name}
</div>
))
因為:
id = 2
不管 Banana 排在:
第 1 個
第 2 個
第 10 個
它還是:
id = 2
所以:
key 最重要的不是唯一而已,還要穩定。
key 要符合兩件事可以先記:
① 同一個列表裡要唯一
② 同一筆資料跨 Render 要穩定
例如:
key={item.id}
通常很好。
因為:
唯一
+
穩定
例如:
key={Math.random()}
這看起來每一筆都一定不同。
但問題是:
每次 Render 都會重新產生新的值。
所以:
第一次 Render
Apple → 0.123
第二次 Render
Apple → 0.987
React 會覺得:
這不是原本那一筆,是新的。
這樣反而失去 key 的意義。
index 什麼時候不是完全不能用?這裡也不是要說:
index永遠不能當 key。
如果列表:
完全不會重新排序
不會刪除
不會插入
每一筆也沒有複雜的內部狀態
那用 index 的風險比較低。
但只要列表可能:
新增
刪除
排序
Filter
Drag & Drop
就應該優先找真正穩定的 ID。
rowKey?實際專案裡,如果使用:
<Table />
或 ProTable,
常看到:
rowKey="id"
這其實跟 React 的 key 概念非常接近。
Table 需要知道:
每一列資料到底是誰。
例如:
[
{
id: 1,
name: "Apple",
},
{
id: 2,
name: "Banana",
},
]
可以:
<Table
rowKey="id"
dataSource={data}
/>
也就是:
id = 1
→ 第一筆資料的 identity
id = 2
→ 第二筆資料的 identity
id 也不一定真的唯一這也是我後來才遇到的問題。
有時候 API 回來的:
id
在目前這個列表裡,竟然可能重複。
例如資料可能來自:
不同來源
不同版本
不同子項目
但剛好共用同一個:
id
這時候如果:
rowKey="id"
就可能出現:
Duplicate key
或列表更新行為異常。
這時候真正要問的是:
這一列資料真正的唯一身份是什麼?
有時可能需要複合 key。
例如:
rowKey={record =>
`${record.documentId}-${record.operationTime}`
}
概念就是:
documentId
+
operationTime
↓
一起組成唯一識別
不是說一定要這兩個欄位。
而是:
如果單一欄位不足以代表一筆資料,就要找能真正區分每一列的組合。
rowKey 不是「隨便挑一個欄位」例如:
rowKey="name"
如果:
Apple
Apple
Banana
兩筆 Apple 名字一樣,
那:
name
就不是好的 identity。
所以選 key / rowKey 時,我現在會問:
這個值在這個列表裡真的唯一嗎?
重新取得資料後還會一樣嗎?
排序之後還能代表同一筆嗎?
key 跟畫面顯示內容是兩回事例如:
<li key={item.id}>
{item.name}
</li>
使用者看到的是:
item.name
React 用來辨認的是:
item.id
所以 key 通常不是拿來顯示的。
它比較像 React 自己在背後使用的 identity。
key 不會出現在 Props 裡?例如:
<UserCard
key={user.id}
name={user.name}
/>
在 UserCard 裡:
function UserCard(props) {
console.log(props.key);
}
不能把 key 當成一般 Props 使用。
因為:
key
是 React 自己使用的特殊屬性。
如果 Component 自己也需要這個 id,要另外傳:
<UserCard
key={user.id}
userId={user.id}
name={user.name}
/>
這時:
props.userId
才能正常使用。
例如:
data.map(...)
或:
<Table dataSource={data} />
我會先問:
① 每一筆真正唯一的資料是什麼?
② 排序 / 刪除 / 新增之後,它還穩定嗎?
③ 現在使用的 key 是資料 identity,
還是只是當下的位置?
如果答案只是:
index
我就會多看一眼:
這個列表之後會不會變動?
我們最近其實一直在處理:
資料如何被辨認
Day 15:
map()
→ 每一筆資料轉成 UI
Day 16:
Spread
→ 建立新的資料
Day 17:
Immutability
→ State 更新時保留清楚的變化
Day 18:
key
→ Render List 時辨認每一筆是誰
所以 React 不只是:
把資料畫出來。
它還需要知道:
哪些是舊資料?
哪些是新資料?
哪一筆被刪除?
哪一筆只是換位置?
哪一筆真的被更新?
而:
key / rowKey
就是其中很重要的一環。
key 不是:
為了消除 React Warning 隨便塞一個值。
而是:
幫 React 穩定辨認列表裡每一筆資料的 Identity。
所以理想的 key:
唯一
+
穩定
比起:
key={index}
更重要的是找到:
key={item.id}
或真正能代表該筆資料的唯一組合。
當我理解這件事之後,再遇到:
Duplicate key
rowKey
列表刪除後狀態跑掉
就不會只把它當成 React 在找麻煩。
而是會先問:
React 現在到底靠什麼判斷「這一列就是這一列」?
下一篇可以繼續接列表另一個很常見的實戰問題:
為什麼 Filter 展開 / 收合,有時候竟然會讓 API 重新打一次?Component 到底什麼時候被重新建立了?