文章

為什麼 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 很重要嗎?

根據下圖可以想看看,要將兩邊的色圈進行交換,你會怎麼做呢?

如果簡單想一下就會發現有兩種可能:

  1. 位置互換:紅圈、藍圈相互交換
  2. 顏色改變:將紅色改成藍色,藍色改成紅色

儘管這兩種方式在單純將色圈交換時看起來是一樣的,但當色圈存在著某種的狀態 (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 出什麼?

import React from 'react'
import Colors from './Colors.js'

export default () => {
  return <Colors />
}

我們可以看到在點擊新增後 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.jsuseEffect 改成

    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. 透過點擊事件在最後新增一個黑色 (加入狀態)

在按下新增按鈕之前大家可以想像一下裡面的狀態會是什麼?

import React from 'react'
import Colors from './Colors.js'

export default () => {
  return <Colors />
}

就像前面所說的,由於 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

如果您喜歡這篇文章,請點擊下方按鈕分享給更多人,這將是對筆者創作的最大支持和鼓勵。