金鑰
在第一章中,我們看到 Preact 如何使用虛擬 DOM 來計算我們 JSX 描述的兩個樹之間的變更,然後將這些變更套用至 HTML DOM 以更新頁面。這在大部分情況下都能順利運作,但偶爾需要 Preact「猜測」樹的形狀在兩次渲染之間的變更。
Preact 的猜測最有可能與我們的意圖不同的情況,是在比較清單時。考慮一個簡單的待辦事項清單元件
export default function TodoList() {
const [todos, setTodos] = useState(['wake up', 'make bed'])
function wakeUp() {
setTodos(['make bed'])
}
return (
<div>
<ul>
{todos.map(todo => (
<li>{todo}</li>
))}
</ul>
<button onClick={wakeUp}>I'm Awake!</button>
</div>
)
}
第一次渲染此元件時,將繪製兩個 <li>
清單項目。在按一下「我醒了!」按鈕後,我們的 todos
狀態陣列會更新,只包含第二個項目 「整理床鋪」
。
以下是 Preact 在第一次和第二次渲染中「看到」的內容
第一次渲染 | 第二次渲染 |
---|---|
|
|
發現問題了嗎?雖然對我們來說很明顯,第一個清單項目(「起床」)已移除,但 Preact 並不知道。Preact 只看到原本有兩個項目,現在只有一個。在套用此更新時,它實際上會移除第二個項目(<li>整理床鋪</li>
),然後將第一個項目的文字從 起床
更新為 整理床鋪
。
結果在技術上是正確的,只有一個項目,文字為「整理床鋪」,但我們得到這個結果的方式並非最佳。想像一下如果有 1000 個清單項目,而我們移除第一個項目:Preact 實際上並非移除一個 <li>
,而是會更新前 999 個項目的文字,然後移除最後一個。
關鍵在於清單渲染
在類似先前範例的情況中,項目會變更順序。我們需要一種方法讓 Preact 知道哪些項目是什麼項目,這樣它才能偵測到每個項目何時新增、移除或取代。為此,我們可以為每個項目新增一個 key
屬性。
key
屬性是給定元素的識別碼。元素不會根據兩個樹狀結構之間的順序進行比較,而是根據 key
屬性來比較具有相同 key
屬性值的先前元素。key
可以是任何類型的值,只要在渲染之間保持「穩定」即可:重複渲染同一項目應該具有完全相同的 key
屬性值。
讓我們為先前範例新增金鑰。由於我們的待辦事項清單是一個不會變更的簡單字串陣列,因此我們可以使用這些字串作為金鑰
export default function TodoList() {
const [todos, setTodos] = useState(['wake up', 'make bed'])
function wakeUp() {
setTodos(['make bed'])
}
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
// ^^^^^^^^^^ adding a key prop
))}
</ul>
<button onClick={wakeUp}>I'm Awake!</button>
</div>
)
}
我們第一次渲染這個新版本的 <TodoList>
元件時,會繪製兩個 <li>
項目。當按下「我醒了!」按鈕時,我們的 todos
狀態陣列會更新,只包含第二個項目「整理床鋪」
以下是我們為清單項目新增 key
之後,Preact 所看到的內容
第一次渲染 | 第二次渲染 |
---|---|
|
|
這次,Preact 可以看出第一個項目已移除,因為第二個樹狀結構缺少具有 key="wake up"
的項目。它會移除第一個項目,並讓第二個項目保持不變。
什麼時候不使用金鑰
開發人員在使用金鑰時最常遇到的陷阱之一,就是意外選擇在渲染之間不穩定的金鑰。在我們的範例中,假設我們使用 map()
的索引引數作為 key
值,而不是 item
字串本身
items.map((item, index) => <li key={index}>{item}</li>
這將導致 Preact 在第一次和第二次渲染時看到以下樹狀結構
第一次渲染 | 第二次渲染 |
---|---|
|
|
問題在於index
實際上並未識別我們清單中的值,而是識別位置。以這種方式呈現實際上迫使 Preact 按順序比對項目,這正是如果沒有鍵的情況下它會執行的動作。使用索引鍵甚至可能在套用於具有不同類型的清單項目時強制昂貴或中斷的輸出,因為鍵無法比對具有不同類型的元素。
🚙 類比時間!想像你將車子停在代客泊車場。
當你回來取車時,你告訴代客泊車人員你開的是一輛灰色的 SUV。不幸的是,停放的車輛中有一半以上是灰色的 SUV,你最後拿到了別人的車。下一個灰色的 SUV 車主拿錯了車,以此類推。
如果你改告訴代客泊車人員你開的是一輛車牌為「PR3ACT」的灰色 SUV,你可以確定你的車會被歸還。
一般來說,切勿將陣列或迴圈索引用作key
。使用清單項目值本身,或為項目產生一個唯一的 ID 並使用它
const todos = [
{ id: 1, text: 'wake up' },
{ id: 2, text: 'make bed' }
]
export default function ToDos() {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
)
}
請記住:如果你真的找不到穩定的鍵,最好完全省略key
屬性,而不是將索引用作鍵。
試試看!
對於本章節的練習,我們將結合我們從上一章節學到的關於鍵的知識,以及我們對副作用的了解。
使用一個效果在<TodoList>
首次呈現後呼叫提供的 getTodos()
函數。請注意,此函數會傳回一個 Promise,你可以透過呼叫 .then(value => { })
來取得其值。一旦你取得 Promise 的值,請透過呼叫它相關聯的 setTodos
方法將其儲存在 todos
useState 鉤子中。
最後,更新 JSX 以將 todos
中的每個項目呈現為包含該 todo 項目 .text
屬性值的 <li>
。