logo

【key編】Reactアンチパターン

2023-06-24
a year ago

開発環境

  • react 18.2.0

前提

Reactの新しいドキュメントであるreact.devを眺めていて、何気なくアンチパターンの実装をしてしまっていることに気づいた今日この頃です。

基本的な内容にはなりますが、ゴリゴリReactを書いている人も一度目を通してもらえると嬉しいです。

本題

この記事で紹介するのはkeyについてです。

配列データのレンダリング時にkeyを設定していないと怒られる、あのkeyです。

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

そもそもなぜkeyが必要か?

ドキュメントから引用すると以下のようにあります。

Imagine that files on your desktop didn’t have names. Instead, you’d refer to them by their order — the first file, the second file, and so on. You could get used to it, but once you delete a file, it would get confusing. The second file would become the first file, the third file would be the second file, and so on.

要するに、Reactはkeyを設定することで、配列の位置や、それ以上の情報を得ることができ、例え並べ替えや削除が起きても正確に把握することができるということです。

逆に、もしkeyを適切に設定せずに一覧で表示するデータを追加、削除するとおかしな挙動を示します。

実際にどんな挙動になるのか、体験してみたい方をこちらから確認してみてください。

鍵の生成にはルールがある

ドキュメントの内容を日本語訳すると、以下の2つのルールがあります。

  • キーはリスト間で一意である必要があります。ただし、異なる配列のJSX ノードに同じキーを使用しても問題ありません。
  • キーを変更したり、その目的を損なったりしてはなりません。レンダリング中にそれらを生成しないでください。

ここで見落としがちなのは2番目の「キーを変更したり、その目的を損なったりしてはなりません。レンダリング中にそれらを生成しないでください」です。

アンチパターン①

不要なkeyの再生成でパフォーマンスを悪くするケースがあります。

これは、データベースに格納しているデータをフェッチして表示する、というよくある一連の流れの中ではあまり起きないです。

なぜなら、上記のケースはデータ自体が一意のidを保持していることがほとんどなので、素直にkeyidを設定してあげればOKです。

ただ、idなど一意の値を持っていないデータを扱うケースも少なからずあります。

そういったときに、このアンチパターンは起きる可能性があります。

例えば以下のように、コンポーネント内でidを生成してリストのデータにセットするパターンです。

import { v4 as uuid } from 'uuid'

export const TaskList = () => {
  const tasks = [
    { id: uuid(), title: 'taskA' },
    { id: uuid(), title: 'taskB' },
    { id: uuid(), title: 'taskC' },
  ];

  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          {task.title}
        </li>
      ))}
    </ul>
  )
}

これは、Reactから怒られることもなく、一見問題なさそうですが、先ほど紹介した鍵の生成ルールに違反しています。

なぜなら、コンポーネントのレンダリングサイクル内でuuidを生成しているので、レンダリングが発生するとidが再生成されるます。よって、キーを変更したり…のルールに違反します。

この場合、Reactはレンダリングごとに新しいデータが作られたんだな、と解釈して毎回DOMツリーを更新します。

また、その要素内で仮にユーザー入力などを行っているのであれば、その入力は失われます。


ちなみに、こういうケースはコンポーネントの外においてあげれば改善できます。

import { v4 as uuid } from 'uuid'

const tasks = [
  { id: uuid(), title: 'taskA' },
  { id: uuid(), title: 'taskB' },
  { id: uuid(), title: 'taskC' },
];

export const TaskList = () => {
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          {task.title}
        </li>
      ))}
    </ul>
  )
}

また、JSXの中でこのようにkeyを生成するのも上記の例と同じく、ダメです。

<li key={Math.random()}>

アンチパターン②

インデックスはダメ。

これは、結構使っている人多いのかなと思いますが、キーを変更したり…のルールに違反します。

例えば、以下のような実装です。

export const TaskList = () => {
  const tasks = [
    { title: 'taskA' },
    { title: 'taskB' },
    { title: 'taskC' },
  ];

  return (
    <ul>
      {tasks.map((task, index) => (
        <li key={index}>
          {task.title}
        </li>
      ))}
    </ul>
  )
}

こちらは公式ドキュメントでも「落とし穴」として紹介されています。

リストの項目が挿入、削除、並び替えなどの変更された場合、項目をレンダリングする順序は時間の経過とともに変化します。

インデックスをキーとして使用すると、微妙で混乱を招くバグが発生することがよくあります。

さいごに

公式ドキュメントが刷新されて、改めて見返してみると気付かされることが多いなと思いました。

定期的に見直したいですね。

参照