キー
第1章では、Preactが仮想DOMを使って、JSXで記述された2つのツリー間で変更された内容を計算し、その変更をHTML DOMに適用してページを更新する方法を見てきました。これはほとんどのシナリオでうまく機能しますが、Preactが2回のレンダリング間でツリーの形状がどのように変化したかを「推測」する必要がある場合もあります。
Preactの推測が私たちの意図と異なる可能性が最も高いシナリオは、リストを比較するときです。簡単なto-doリストコンポーネントを考えてみましょう。
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>
)
}
このコンポーネントが最初にレンダリングされると、2つの<li>
リスト項目が描画されます。「起きた!」ボタンをクリックすると、todos
ステート配列が更新され、2番目の項目である"make bed"
のみが含まれるようになります。
以下は、最初のレンダリングと2回目のレンダリングでPreactが「見る」ものです。
最初のレンダリング | 2回目のレンダリング |
---|---|
|
|
問題に気づきましたか?最初のリスト項目(「wake up」)が削除されたことは私たちには明らかですが、Preactはそれを知りません。Preactが見ているのは、2つの項目があり、現在は1つしかないということです。この更新を適用すると、実際には2番目の項目(<li>make bed</li>
)を削除し、最初の項目のテキストをwake up
からmake bed
に更新します。
結果は技術的には正しい(テキストが「make bed」の単一の項目)ですが、その結果に至った方法は最適ではありませんでした。1000個のリスト項目があり、最初の項目を削除した場合を想像してみてください。Preactは単一の<li>
を削除する代わりに、他の最初の999個の項目のテキストを更新し、最後の項目を削除します。
リストレンダリングのキー
前の例のような状況では、項目が順序を変えています。各項目が追加、削除、または置換されたことを検出できるように、Preactがどの項目がどれであるかを知るのを助ける方法が必要です。これを行うには、各項目にkey
プロパティを追加できます。
key
プロパティは、特定の要素の識別子です。2つのツリー間の要素の順序を比較する代わりに、key
プロパティを持つ要素は、同じkey
プロパティ値を持つ前の要素を見つけることで比較されます。key
は、レンダリング間で「安定」している限り、任意の値の型にすることができます。同じ項目の繰り返しレンダリングは、まったく同じkey
プロパティ値を持つ必要があります。
前の例にキーを追加してみましょう。私たちのtodoリストは変更されない単純な文字列の配列なので、これらの文字列をキーとして使用できます。
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>
コンポーネントを最初にレンダリングすると、2つの<li>
項目が描画されます。「起きた!」ボタンをクリックすると、todos
ステート配列が更新され、2番目の項目である"make bed"
のみが含まれるようになります。
リスト項目にkey
を追加したため、Preactが現在見ているのは次のとおりです。
最初のレンダリング | 2回目のレンダリング |
---|---|
|
|
今回は、2番目のツリーにkey="wake up"
の項目がないため、Preactは最初の項目が削除されたことを確認できます。最初の項目を削除し、2番目の項目は変更せずに残します。
キーを使用しない場合
開発者がキーで最もよく遭遇する落とし穴の1つは、レンダリング間で不安定なキーを誤って選択することです。私たちの例では、item
文字列自体ではなく、map()
からのインデックス引数をkey
値として使用した場合を想像してください。
items.map((item, index) => <li key={index}>{item}</li>
これにより、Preactは最初と2回目のレンダリングで次のツリーを認識します。
最初のレンダリング | 2回目のレンダリング |
---|---|
|
|
問題は、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>
としてレンダリングします。