Reactのkey propに配列のindexを使うことが良くない理由

4 min read読了の目安(約4200字

始め

Reactのmapを使う時、keyエラーをなくすためindexを使ったことがあります。しかし最近それがanti-patternだということを知りましたので、その理由をまとめました。


1. keyの存在意義

1-1. keyってなんだっけ

そういえばそもそもkeyって何で必要だったけ…?と、ふいと思ってしまいました。何となくは知ってますが、明確にしたいのでこの部分から始めましょう。

まずはこのサンプルコードをご覧ください。

export default function App() {
  let fruits = [{ name: "apple" }, { name: "banana" }, { name: "pear" }];
  return (
    <div className="App">
      {fruits.map((fruit) => (
        <p>{fruit.name}</p>
      ))}
    </div>
  );
}

fruitsという配列をmapメソッドを使ってpタグに入れてる簡単なコードです。これだけ見たら問題なさそうに見えますし、画面にもちゃんとでます。

しかし、console窓を確認したらこういうエラーが起きてました。

Warning: Each child in a list should have a unique "key" prop.

リスト内の各childには、固有のkey propが必要です」という意味ですね。まだkeyが何なのか曖昧ですが、とりあえず固有のkeyが必要だと言ってるので入れます。

export default function App() {
  let fruits = [
    { id: 1, name: "apple" },
    { id: 2, name: "banana" },
    { id: 3, name: "pear" }
  ];
  return (
    <div className="App">
      {fruits.map((fruit) => (
        <p key={fruit.id}>{fruit.name}</p>
      ))}
    </div>
  );
}

key propidを入れたら先のエラーが消えました。

keyが何をするやつなのかはReactの公式ドキュメントでも説明しています。

Key は、どの要素が変更、追加もしくは削除されたのかを React が識別するのに役立ちます。配列内の項目に安定した識別性を与えるため、それぞれの項目に key を与えるべきです。

この説明だけではピンとこないかもしれませんので、上のサンプルコードで例をあげてみます。

1-2. keyがない場合

fruits{ name: "apple" }{ name: "banana" }の間に{ name: "melon" }を追加します。動的に追加するのが普通ですが、わかりやすくするためにサンプルコードには追加された結果を入れました。

export default function App() {
  let fruits = [
    { name: "apple" },
    { name: "melon" }, // 新しく追加
    { name: "banana" },
    { name: "pear" }
  ];
  return (
    <div className="App">
      {fruits.map((fruit) => (
        <p>{fruit.name}</p>
      ))}
    </div>
  );
}

皆さんご存知の通り、Reactは変更がある時に変更前と後を比較して変更がある部分だけ更新させます。ここで大事なのは、基本的に比較するときは上から順番に比較するということです

この変化を反映させたら何が起こるかを絵で見てみましょう。

②の左が変更前、右が変更後です。上から順番に比較された結果、bananamelonに変更、pearbananaに変更、新規pear追加の3つの更新が行われます。配列の中間に新しい要素一つ入れただけなのに、実際には変化のない要素まで更新されるなんて非効率的に感じますね。

1-3. keyがある場合

export default function App() {
  let fruits = [
    { id: 1, name: "apple" },
    { id: 4, name: "melon" }, // 新しく追加
    { id: 2, name: "banana" },
    { id: 3, name: "pear" }
  ];
  return (
    <div className="App">
      {fruits.map((fruit) => (
        <p key={fruit.id}>{fruit.name}</p>
      ))}
    </div>
  );
}

ここでkeyが活躍してくれます。keyをつけると、Reactが比較するときにkeyを元に比較してくれます。つまり、上から順番通りではなくてkeyが同じ要素同士に比較するということです。結果、以下のように改善されます。

keyid入れただけなのに前回とは違って新規で追加された要素一つだけ更新されました。これで簡単に不要な更新を防げられます。

そして、ここまで理解したら薄々keyにindexを入れたら何かまずそうな気がしてきます

2. indexが危険な理由

Robin Pokornyの「Index as a key is an anti-pattern」良いデモがありましたので、試してみました。皆さんもやってみてください。

keyindexの場合とidの場合の違い、わかりましたか?

私はきっとFooなんちゃらのinputに数字を入力したのに、その上に新しいinputを追加したら入力した内容も新しいinputに行ってしまいました。

理由は簡単です。私が書いた「12345」は1番最初のinputなのでindex0、つまりkey=0になってるはずです。しかし、先頭に新規inputを追加したらそれが0番目のindexkey=0になってしまいます。Reactは「12345」はkey=0inputのものだと判断します。結果、先頭に新規欄を追加したら入力内容がそこに行ってしまうということでしょう。

React公式ドキュメントでもこのような場合について言及しています。

要素の並び順が変更される可能性がある場合、インデックスを key として使用することはお勧めしません。パフォーマンスに悪い影響を与え、コンポーネントの状態に問題を起こす可能性があります。

3. まとめ

今までkeyindexを使うことは良くないと話しました。しかし、何があっても絶対使ってはいけないわけでもありませんReact公式ドキュメントではkeyindexを使うことは最終手段だと表現してます。でしたら、indexもオッケーな場合はいつでしょうか?

「Index as a key is an anti-pattern」では、「以下の3つの条件をすべて満たしたらkeyindexを使ってもも安全でしょう」と説明しました。

  1. 配列とその中の要素が静的(計算も変更もされない)
  2. 配列の中の要素がidを持ってない
  3. 配列がreorderfilterされることが絶対ない

ですが、やはりできるだけkeyにはidなどの変わらない固有の値をを使ったほうがいいと思います。indexにしたらいつどこでバグるかもしれないという不安が残りますから。


終わり

割と簡単な内容でしたが、ふわふわしてた部分をはっきりできてよかったです😌