為什麼 React 需要 key?
- 文章發表於
作為一名 React 的開發者, 你可能曾經收到提醒你要記得放 key 的警示!
Warning: Each child in a list should have a unique "key" prop
有些開發者可能就直接傳入 index
作為 key,但這其實解決不了問題,同時這樣做也是 React 的 anti-pattern。
本篇文章將會透過一些例子來解釋為什麼 React 需要 key,以及為什麼不能用 index
作為 key。
key 很重要嗎?
根據下圖可以想看看,要將兩邊的色圈進行交換,你會怎麼做呢?
如果簡單想一下就會發現有兩種可能:
- 位置互換:紅圈、藍圈相互交換
- 顏色改變:將紅色改成藍色,藍色改成紅色
儘管這兩種方式在單純將色圈交換時看起來是一樣的,但當色圈存在著某種的狀態 (e.g. 記錄被點擊次數),那這樣 位置交換 跟 顏色改變 就有很大的差異了!
想像一下紅圈被點擊次數是 10, 藍圈被點擊次數為 5,我們透過下圖來了其何不同:
位置互換 | 顏色改變 |
---|---|
位置互換會將被點擊次數一起互換,而顏色改變則是只有改變顏色,其被點擊次數依然保留在原本的位置。
這也是為什麼 React 需要 key 的原因,因為 React 不知道你要做的是 位置互換 還是 顏色改變,所以我們需要 key
來告訴 React 這個 Element 的唯一性,讓 React 能夠正確的進行更新。
為什麼 React 需要 key ?
假設現在有三個為一組的色圈,分別為 紅、藍 與 黃,並且透過 map
渲染出該組色圈 (如下圖)
// 示意結構<div><Color color="red" /><Color color="blue" /><Color color="yellow" /></div>
兩種情境理解 React 的渲染機制
接下來我們透過兩種情境來驗證當 沒有 放 key 時 React 的渲染機制
1. 透過點擊事件在最後新增一個黑色
頁面渲染三色後,我們再透過點擊事件在最後新增一個黑色,大家可以想想會 log 出什麼?
我們可以看到在點擊新增後 console 會再 log 出一個 black mount
(可以開啟 chrome devtool 的 console 查看)
---在最後新增一個黑色---black mount
這裡可以想像成當點擊新增後,React 會將變化的部分的 Tree 重新建構並且與改變前的 Tree 進行比較,React 會比較每個 item 的位置,決定是否有沒有需要打掉重建,或是只要改變某些屬性就好,這個過程我們稱作 reconciliation。
在這個情境下, React 在 reconciliation 時,發現前三個色圈都跟之前一樣,心想太棒了,省事省事,所以前三個 Element 保留,到黑色時發現是新增的,所以就創建黑圈在最後。
<div><Color color="red" /><Color color="blue" /><Color color="yellow" /><Color color="black" /> // 新增的黑色</div>
2. 透過點擊事件在最前方新增一個黑色
在第二個情境,讀者們可以將上面的程式碼稍作修改:
將
Colors.js
的第 10 行改成setColors(['black', ...COLORS])Color.js
的useEffect
改成React.useEffect(() => {console.log(`---最前方新增一個黑色--- \n ${color} mount`)return () => {console.log(`---最前方新增一個黑色--- \n ${color} unmount`)}}, [color])清掉 console,再點擊重整按鈕
此時我們看到 console 印出了 3 次 unmount 以及 4 次 mount! 也就是說 React 在 reconciliation 時發現第一個跟之前不一樣,所以就把所有的 React Element 打掉重建。
加入狀態
理解 React 的渲染機制後,現在我們在把有狀態時的情境加進來,假設這組色圈都本身都記錄了被點擊次數,而 React 會用 key 去辨識是否需要重新渲染該 Element 與保留該 Elment 正確的狀態,如果沒有提供 key 就會 fallback 使用 array 的 index 作為 key!
<div><Color key={0} color="red" /><Color key={1} color="blue" /><Color key={2} color="yellow" /></div>
接下來我們將狀態加入上述兩種情境
1. 透過點擊事件在最後新增一個黑色 (加入狀態)
在按下新增按鈕之前大家可以想像一下裡面的狀態會是什麼?
就像前面所說的,由於 React 在 reconciliation 時看到前面三個色圈是一樣的,所以不會打掉重建,並且 key 恰好也是相同的,所以裡面的點擊次數也會是正確的。
2. 透過點擊事件在最前方新增一個黑色 (加入狀態)
那在前方新增一個黑色呢?大家可以將照著前面程式碼修改的步驟,完成後點擊新增按鈕,看看 console 會印出什麼?
點擊新增後我們可以看到,因為我們沒有提供 key,所以 React 將 Element 重新建立後,卻不知道狀態是哪個 Element 的,所以只能用 children array 的 index 將 state 塞回去 ,導致 state 亂掉的現象。
<div><Color key={0} color="black" /><Color key={1} color="red" /><Color key={2} color="blue" /><Color key={3} color="yellow" /></div>
3. 加入 key
我們可以在 Colors.js
的第 17 行將 key 加入
<Color color={c} key={c} />
就可以看到一切都會是正常的了,因為我們提供了唯一的 key,讓 React 知道那些項目需要被 刪除 / 更新 / 插入,這也是我們需要 唯一 key 的原因,有了它,React 不但可以省去重建 Element 所花的時間,也可以將 state 完整保存!
<div><Color key="black" color="black" /><Color key="red" color="red" /><Color key="blue" color="blue" /><Color key="yellow" color="yellow" /></div>
結論
記得要放唯一 key !!!
最後用 playground 來做個小結吧!在 playground 內可以看到下列三種結果
- 沒有放 key
- 用 index 作為 key
- 用唯一值作為 key
按下 shuffle 之後前兩者都不會將原項目的 state 進行轉移,只有使用唯一值作為 key 的方式,才會將原項目的 state 一起變動!
import React from 'react' import Example from './Example.js' import { shuffle, COLORS } from './tool.js' const App = () => { const [colors, setColors] = React.useState(COLORS) return ( <> <button className="shuffle" onClick={() => setColors(shuffle(COLORS))}> shuffle </button> <Example colors={colors} /> </> ) } export default App