フロントエンド

リストのkeyにindexを使うと何が起きるか

読了 6分
フロントエンド

リストのkeyにindexを使うと何が起きるか

Reactで配列を描画すると、コンソールに Each child in a list should have a unique "key" prop. という警告が出ます。これをとりあえず黙らせようと key={index} を渡して、あとで地味なバグに悩まされたので、そのときのメモです。

なぜ key が必要で、なぜindexだとまずいのかを順に整理します。

keyはReactにとっての「同一性の目印」

Reactは再描画のたびに、前回のリストと今回のリストを突き合わせて「どの要素が増えた・減った・移動した」かを判断します。このときの照合に使うのが key です。key が同じなら「同じ要素が続いている」とみなして、対応するDOMやコンポーネントのstateを再利用します。

つまり key は「リスト内のどの項目か」を一意に指す 安定した識別子 である必要があります。データ自体が持つIDがあれば、それが最適です。

{users.map((user) => (
  <UserRow key={user.id} user={user} />
))}

indexがまずいのは「順番が変わる」とき

問題は、key に配列のindexを使った場合です。indexは「今その要素が何番目にあるか」でしかなくて、項目そのものには結びついていません。並べ替えや先頭への挿入が起きると、同じ項目でもindexが変わってしまいます。

具体的な壊れ方を見てみます。各行が入力欄を持つフォームで、先頭に行を追加するケースです。

function TodoList() {
  const [todos, setTodos] = useState(['牛乳を買う', '本を返す'])

  const addToTop = () => setTodos(['(新規)', ...todos])

  return (
    <div>
      <button onClick={addToTop}>先頭に追加</button>
      {todos.map((todo, index) => (
        // 危険:keyがindex
        <input key={index} defaultValue={todo} />
      ))}
    </div>
  )
}

ここで各 input に何か入力してから「先頭に追加」を押すと、入力した文字が 一つ下の行にずれて残る という挙動になります。最初これを見たとき、何が起きたのか分からず固まりました。

Reactから見ると key=0 の要素はずっと存在し続けている(中身の defaultValue が変わっただけ)と判断されて、input のDOM(=ユーザーが打ち込んだ値)はそのまま再利用されるためです。

defaultValue や、コンポーネント内部の useState、フォーカス状態など「DOM側・React側が保持しているもの」はすべてこのズレの影響を受けます。

安定したkeyを用意する

解決策は、indexではなく 項目に固有で変化しない値 をkeyにすることです。

type Todo = { id: string; text: string }

const [todos, setTodos] = useState<Todo[]>([
  { id: 'a1', text: '牛乳を買う' },
  { id: 'b2', text: '本を返す' },
])

{todos.map((todo) => (
  <input key={todo.id} defaultValue={todo.text} />
))}

サーバー由来のデータならたいていIDがあります。クライアントだけで作る項目には、生成時に crypto.randomUUID() などでIDを振っておくと安心です。

indexをkeyにしてもよい条件

とはいえ、常にindexが悪いわけではないようです。次の3つを すべて 満たすなら、indexでも実害は出ません。

条件内容
並べ替えがない要素の順序が固定されている
末尾以外の追加削除がない途中への挿入や削除をしない
項目がstateを持たない各項目が入力欄や内部stateを抱えていない

静的な表示専用リストなどはこれに当てはまります。逆に、並べ替え・途中挿入・入力欄のどれか一つでもあるなら、固有IDを使うべきだと考えておくとよさそうです。

まとめ

  • key はリスト要素の同一性をReactに伝える目印で、安定して一意であることが前提
  • indexをkeyにすると、並べ替えや先頭挿入でstateやDOMが取り違えられる
  • 原則は項目固有のIDを使う。完全に静的なリストに限ってindexも許容できる

判断に迷ったら「この項目の順序や数は将来変わりうるか?」を問うのが近道だと思います。詳しい挙動はReact公式のRendering Listsにも整理されています。以上です。