ヘルプ
ご支援ください

キー

第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回目のレンダリング
<div>
  <ul>
    <li>wake up</li>
    <li>make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>
<div>
  <ul>
    <li>make bed</li>

  </ul>
  <button>I'm Awake!</button>
</div>

問題に気づきましたか?最初のリスト項目(「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回目のレンダリング
<div>
  <ul>
    <li key="wake up">wake up</li>
    <li key="make bed">make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>
<div>
  <ul>

    <li key="make bed">make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>

今回は、2番目のツリーにkey="wake up"の項目がないため、Preactは最初の項目が削除されたことを確認できます。最初の項目を削除し、2番目の項目は変更せずに残します。

キーを使用しない場合

開発者がキーで最もよく遭遇する落とし穴の1つは、レンダリング間で不安定なキーを誤って選択することです。私たちの例では、item文字列自体ではなく、map()からのインデックス引数をkey値として使用した場合を想像してください。

items.map((item, index) => <li key={index}>{item}</li>

これにより、Preactは最初と2回目のレンダリングで次のツリーを認識します。

最初のレンダリング2回目のレンダリング
<div>
  <ul>
    <li key={0}>wake up</li>
    <li key={1}>make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>
<div>
  <ul>

    <li key={0}>make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>

問題は、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メソッドを呼び出して、todosuseStateフックに保存します。

最後に、JSXを更新して、todosの各項目を、そのtodo項目の.textプロパティ値を含む<li>としてレンダリングします。

読み込み中...