【React】リスト表示とkey属性の重要性

JavaScript

こんにちは、かつコーチです。

前回は条件分岐レンダリングのパターンを紹介しました。

今回は、Reactアプリでほぼ必ず登場する「配列データを一覧表示する」方法を扱います。

セットで必要になる key 属性についても、なぜ必要なのかをきちんと理解しておきましょう。

リスト表示とは?

map()で配列をJSXに変換する

Reactでは、配列の中身を一覧表示したいとき、JavaScriptの map() メソッドを使ってJSXの配列に変換します。

interface User {
  id: number;
  name: string;
}

function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

users.map((user) => <li>...</li>) のように、配列の各要素をJSX要素に変換した配列を作り、それを { } の中に埋め込んで表示しています。

VueやAngularのような専用のループ構文はなく、あくまで「JavaScriptの配列操作をJSXの中で使う」だけというのがReactらしいポイントです。

なぜkey属性が必要なのか

先ほどのコード例で、<li> タグに key={user.id} という見慣れない属性が付いていることに気づいたと思います。

Reactは、リストの中身が変化したとき(追加・削除・並び替え)に、どの要素が変わったのかを効率よく判断するために key を利用します。

key がないと、Reactは「配列のどの項目が、画面上のどのDOM要素に対応するのか」を正確に把握できず、意図しない挙動や無駄な再描画につながることがあります。

基本の書き方

map()とkeyのセットが基本形

map() でリストを描画するときは、必ず一番外側の要素に key を指定します。

function TodoList({ todos }: { todos: { id: number; text: string }[] }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

key はコンポーネントの中で表示に使われるわけではなく、あくまでReact内部が要素を識別するための特別なPropsです。

そのため todo.key のようにコンポーネント側で参照することはできません。

keyには何を使うべきか

key に指定する値は、そのリストの中で一意な値である必要があります。

一般的には、データベースのIDなど、要素ごとに変わらない一意な値を使うのが基本です。

// 良い例:データが持つ一意なIDを使う
{todos.map((todo) => (
  <li key={todo.id}>{todo.text}</li>
))}

APIから取得したデータであれば id フィールドがそのまま使えることが多く、迷ったらまずIDを探すのが基本方針になります。

よくあるつまずきポイント:keyにindexを使ってしまう

実際にハマった並び替えバグ

IDが用意されていないデータをリスト表示するとき、key に配列のインデックス(index)を使ってしまうケースがよくあります。

私も以前、並び替え機能付きのTodoリストを作っていて、このindexを使ったことで思わぬバグに苦しんだ経験があります。

❌ Before:keyに配列のindexを使う

function TodoList({ todos }: { todos: { text: string; done: boolean }[] }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          <input type="checkbox" defaultChecked={todo.done} />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

一見動いているように見えるのですが、リストの先頭に新しいTodoを追加したり、途中の項目を削除したりすると、index の値が全項目でズレてしまいます。

その結果、チェックボックスのチェック状態が別のTodoの内容にすり替わって表示される、という奇妙なバグが発生しました。

key が変わらないまま中身のデータだけがズレてしまうため、Reactが「同じDOM要素を使い回してよい」と誤判断してしまうのが原因です。

デバッグ中は「なぜチェックした覚えのない項目にチェックが付くのか」がまったく分からず、原因特定に丸一日近くかかりました。

✅ After:データが持つ一意なIDをkeyに使う

function TodoList({
  todos,
}: {
  todos: { id: number; text: string; done: boolean }[];
}) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input type="checkbox" defaultChecked={todo.done} />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

各Todoに一意な id を持たせ、それを key に使うことで、並び替えや削除が起きても各要素とデータの対応関係が正しく保たれるようになりました。

index をkeyに使ってよいのは、リストの中身が絶対に追加・削除・並び替えされないと分かっている場合に限られます。

迷ったときは「このリストは並び替わる可能性があるか」を自問し、少しでも可能性があるなら一意なIDを用意するようにしています。

応用:一歩先の使い方

ネストしたリストにもそれぞれkeyを付ける

配列の中にさらに配列がある「ネストしたリスト」の場合も、それぞれの階層でkeyが必要です。

interface Category {
  id: number;
  name: string;
  items: { id: number; label: string }[];
}

function CategoryList({ categories }: { categories: Category[] }) {
  return (
    <div>
      {categories.map((category) => (
        <div key={category.id}>
          <h3>{category.name}</h3>
          <ul>
            {category.items.map((item) => (
              <li key={item.id}>{item.label}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

外側の category.id と内側の item.id は、それぞれ別のスコープの key として独立しているため、同じ数値が重複しても問題ありません(同じ配列内でだけ一意であればよいというルールです)。

filterと組み合わせて絞り込み表示する

map() の前に filter() を挟むと、条件に合うデータだけを一覧表示できます。

function ActiveTodoList({
  todos,
}: {
  todos: { id: number; text: string; done: boolean }[];
}) {
  const activeTodos = todos.filter((todo) => !todo.done);

  return (
    <ul>
      {activeTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

filter() で未完了のTodoだけに絞り込んでから map() を呼ぶことで、条件分岐レンダリングとリスト表示を自然に組み合わせられます。

まとめ

この記事のポイント

  • 配列データを一覧表示するには map() を使ってJSXの配列に変換する
  • key はReactが各要素を正しく識別するための特別なPropsで、リストの外側要素には必須
  • key にはデータが持つ一意なIDを使うのが基本
  • 配列のindexをkeyに使うと、並び替え・追加・削除時に表示がずれるバグにつながる
  • filter()map() を組み合わせれば、条件に合うデータだけを一覧表示できる

次に読むべき記事

リスト表示ができるようになったら、次はユーザーからの入力を受け取る「フォーム」の作り方に進みます。

次回は、Reactの基本であり最初につまずきやすいポイントでもある「制御コンポーネント」を解説します。

→ 次の記事:フォームの作り方:制御コンポーネントの基本

タイトルとURLをコピーしました