モダンJavaScriptの基礎から始める挫折しないためのReact入門 60日コース | Stateを使いこなす - DAY 24:keyを理解する

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 24:keyを理解する 前半 ― 「Reactがリストをどう見ているか」を知る
  2. keyとは何か
    1. 「Reactがリストの各要素を識別するためのラベル」です
  3. なぜkeyが必要なのか
    1. 「リストが変化したときに、正しく差分を取るため」です
  4. IDをkeyに使う ― 一番安全で基本的な方法
    1. 「そのデータを一意に識別できる値」をkeyにする
  5. indexをkeyにする問題
    1. 一見動くが、後で痛い目を見るパターンです
    2. 具体的なイメージ
    3. indexをkeyにすると何が起きるか
  6. 実践:ユーザー一覧を作成しながらkeyを意識する
    1. 基本形:IDをkeyに使ったユーザー一覧
    2. このコードで確認できること
  7. indexをkeyにした場合との違いをイメージする
    1. indexをkeyにしたユーザー一覧(推奨されない例)
  8. 練習用テンプレート:ユーザー一覧+追加・削除
  9. DAY 24 前半のまとめ
  10. DAY 24:keyを理解する 中盤 ― 「動くUI」とkeyの関係を体で理解する
  11. Reactは「前回」と「今回」を比べている
    1. 差分更新という考え方を押さえる
  12. keyとは「ReactにとってのID」である
    1. データのIDと、ReactのIDを一致させる
  13. indexをkeyにしたときに起きる「ズレ」をイメージする
    1. 並びが変わると、indexも変わる
    2. 入力フォーム付きのリストで起きる違和感
  14. IDをkeyにすることで「ズレ」を防ぐ
    1. データのIDは変わらない
  15. 実践:ユーザー一覧でkeyの選び方を意識する
    1. 正しい形:IDをkeyにしたユーザー一覧
    2. indexをkeyにした場合との違いを意識する
  16. 練習用テンプレート:ユーザー一覧+メモ入力付き
  17. DAY 24 中盤のまとめ
  18. DAY 24:keyを理解する 後半 ― 「実務レベルで通用するkeyの感覚」を仕上げる
  19. keyとは何かを「一言で説明できる」ようにする
    1. Reactにとっての「その要素のID」です
  20. なぜkeyが必要かを「差分更新」の視点で整理する
    1. Reactは「前回」と「今回」を比べている
    2. ユーザー一覧でイメージする
  21. IDを使用する ― 「データのID」と「Reactのkey」を揃える
    1. ユーザー一覧での基本パターン
  22. indexをkeyにする問題を「実務的な違和感」として捉える
    1. 見た目は動くが、内部的には不安定
    2. 入力フォーム付きのユーザー一覧での違和感
  23. 実践:ユーザー一覧を作成しながらkeyの設計を仕上げる
    1. 追加・削除・編集ができるユーザー一覧
    2. このコードで確認できること
  24. DAY 24 後半のまとめ

DAY 24:keyを理解する 前半 ― 「Reactがリストをどう見ているか」を知る

DAY 24では、Reactのリスト表示で必ず登場する key を深く理解していきます。 ここは、mapによるリスト表示を「安全に」「正しく」動かすための、とても重要なポイントです。

前半では、次のテーマを軸に進めます。

  • keyとは
  • なぜ必要か
  • IDを使用する
  • indexをkeyにする問題
  • 実践:ユーザー一覧を作成

単に「警告を消すためのもの」として覚えるのではなく、 「Reactがリストをどう認識しているか」を理解するための入り口として、丁寧にかみ砕いていきます。

keyとは何か

「Reactがリストの各要素を識別するためのラベル」です

まず、key の正体を一言で言うと、

「Reactが、リストの各要素を識別するために使うラベル(IDのようなもの)」

です。

mapでリストを表示するとき、よく次のようなコードを書きます。

function UserList() {
  const users = [
    { id: 1, name: "太郎" },
    { id: 2, name: "花子" },
    { id: 3, name: "健" },
  ];

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
JSX

ここでの key={user.id} が、まさに「Reactが各要素を識別するためのラベル」です。

Reactは、リストが更新されたときに、

  • どの要素が追加されたか
  • どの要素が削除されたか
  • どの要素が並び替えられたか

を判断する必要があります。 そのときに使われるのが key です。

なぜkeyが必要なのか

「リストが変化したときに、正しく差分を取るため」です

Reactは、画面を毎回ゼロから描き直しているわけではありません。 「前回の描画」と「今回の描画」の差分を計算して、必要な部分だけ更新する」という仕組みで動いています。

リスト表示の場合、この「差分計算」が少し難しくなります。

例えば、次のようなリストがあるとします。

const users = [
  { id: 1, name: "太郎" },
  { id: 2, name: "花子" },
  { id: 3, name: "健" },
];
JSX

ここから「花子」を削除して、

const users = [
  { id: 1, name: "太郎" },
  { id: 3, name: "健" },
];
JSX

になったとき、Reactはこう考えます。

  • 「太郎」は前回も今回もいる
  • 「花子」はいなくなった
  • 「健」は前回も今回もいる

この「誰が残っていて、誰がいなくなったか」を判断するために、 key が必要になります。

もし key がなければ、Reactは「どの要素がどのデータに対応しているか」を正しく追跡できません。

IDをkeyに使う ― 一番安全で基本的な方法

「そのデータを一意に識別できる値」をkeyにする

最も基本で安全な方法は、

「そのデータを一意に識別できるIDをkeyにする」

ことです。

ユーザー一覧なら、よく次のような形になります。

function UserList() {
  const users = [
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
    { id: 3, name: "健", email: "ken@example.com" },
  ];

  return (
    <div>
      <h2>ユーザー一覧</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <p>名前:{user.name}</p>
            <p>メール:{user.email}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}
JSX

ここでの user.id は、

  • データベースやAPIから渡される「ユーザーID」
  • 各ユーザーごとに一意な値

という前提で使われています。

重要ポイント:

  • keyには「その要素を一意に識別できる値」を使うのが基本です。
  • オブジェクト配列なら、id フィールドをkeyにするのが最もよくあるパターンです。

indexをkeyにする問題

一見動くが、後で痛い目を見るパターンです

mapを使うとき、次のような書き方をしてしまうことがあります。

{users.map((user, index) => (
  <li key={index}>{user.name}</li>
))}
JSX

一見すると動きますし、Reactの警告も消えます。 しかし、これは推奨されない書き方です。

理由は、

「リストの並び替えや削除・追加が行われたときに、indexが変わってしまうから」

です。

具体的なイメージ

例えば、次のようなリストを考えます。

const users = [
  { id: 1, name: "太郎" }, // index 0
  { id: 2, name: "花子" }, // index 1
  { id: 3, name: "健" },   // index 2
];
JSX

ここから「太郎」を削除すると、

const users = [
  { id: 2, name: "花子" }, // index 0
  { id: 3, name: "健" },   // index 1
];
JSX

という状態になります。

indexをkeyにしている場合、

  • 「花子」は前回 index 1 → 今回 index 0
  • 「健」は前回 index 2 → 今回 index 1

というように、同じユーザーなのに、keyが変わってしまいます。

Reactから見ると、

  • 「index 0の要素が新しく現れた」
  • 「index 1の要素も新しく現れた」

というように見えてしまい、 本来は「太郎だけが消えた」状態なのに、 「全部入れ替わった」ように誤解される可能性があります。

indexをkeyにすると何が起きるか

特に次のような場合に問題が顕在化します。

  • 入力フォームを含むリスト
  • チェックボックスやラジオボタンを含むリスト
  • アニメーションやトランジションを含むリスト

indexをkeyにしていると、

  • 入力内容が別の行に移ってしまう
  • チェック状態が意図しない行に残ってしまう
  • アニメーションが不自然に再生される

といった「気持ち悪い挙動」が起きることがあります。

重要ポイント:

  • indexは「配列の並び」に依存する値であり、 並び替えや削除・追加で簡単に変わってしまいます。
  • そのため、「その要素を一意に識別するID」としては不適切です。

実践:ユーザー一覧を作成しながらkeyを意識する

基本形:IDをkeyに使ったユーザー一覧

まずは、IDをkeyに使った正しい形のユーザー一覧を作ってみます。

import { useState } from "react";

function UserList() {
  const [users, setUsers] = useState([
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
    { id: 3, name: "健", email: "ken@example.com" },
  ]);

  const handleDelete = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー一覧</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <p>名前:{user.name}</p>
            <p>メール:{user.email}</p>
            <button onClick={() => handleDelete(user.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;
JSX

このコードで確認できること

  • users は「ユーザー情報のオブジェクト」が入った配列です。
  • map() で各ユーザーをJSXに変換しています。
  • key={user.id} で、各ユーザーを一意に識別しています。
  • 削除ボタンを押すと、setUsers で配列を更新し、
    • Reactが key を使って「どのユーザーが消えたか」を正しく判断します。

ここでの大事な感覚:

  • 「ユーザーID」と「画面上の要素」が、key を通して1対1で結びついている
  • 削除や追加があっても、IDが変わらない限り、そのユーザーに対応する要素は正しく追跡される

indexをkeyにした場合との違いをイメージする

indexをkeyにしたユーザー一覧(推奨されない例)

あえて、indexをkeyにした場合のコードも見ておきます。

import { useState } from "react";

function UserListBad() {
  const [users, setUsers] = useState([
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
    { id: 3, name: "健", email: "ken@example.com" },
  ]);

  const handleDelete = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー一覧(indexをkeyにした例)</h2>
      <ul>
        {users.map((user, index) => (
          <li key={index}>
            <p>名前:{user.name}</p>
            <p>メール:{user.email}</p>
            <button onClick={() => handleDelete(user.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserListBad;
JSX

このコードは一見動きますが、

  • ユーザーを削除したときに、
    • 残ったユーザーのindexが変わってしまう
    • Reactから見ると「別の要素」として扱われる可能性がある

という問題を抱えています。

特に、各ユーザー行に入力フォームやチェックボックスを追加した場合、 「入力内容やチェック状態が別の行に移ってしまう」ような挙動が起きることがあります。

重要ポイント:

  • indexをkeyにすると、「見た目は動いているように見えるが、内部的には不安定な状態」になりがちです。
  • 実際のアプリでは、こうした不安定さがユーザー体験の違和感として現れます。

練習用テンプレート:ユーザー一覧+追加・削除

最後に、DAY 24前半の内容をまとめた練習用テンプレートとして、 「ユーザー一覧+追加・削除」ができるコンポーネントを示します。

import { useState } from "react";

function UserList() {
  const [users, setUsers] = useState([
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
  ]);

  const [nextId, setNextId] = useState(3);

  const handleAddUser = () => {
    const newUser = {
      id: nextId,
      name: `ユーザー${nextId}`,
      email: `user${nextId}@example.com`,
    };
    setUsers((prevUsers) => [...prevUsers, newUser]);
    setNextId((prevId) => prevId + 1);
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー一覧(追加・削除付き)</h2>
      <button onClick={handleAddUser}>ユーザーを追加</button>
      <ul style={{ marginTop: "8px" }}>
        {users.map((user) => (
          <li key={user.id}>
            <p>名前:{user.name}</p>
            <p>メール:{user.email}</p>
            <button onClick={() => handleDeleteUser(user.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;
JSX

このテンプレートの中には、

  • オブジェクト配列としてのユーザー一覧
  • map() によるリスト表示
  • JSXでの表示構造
  • key={user.id} による一意な識別
  • Stateによる追加・削除の管理

が自然に含まれています。

DAY 24 前半のまとめ

DAY 24の前半では、

  • key が「Reactがリストの各要素を識別するためのラベル」であり、差分計算のために必須であること
  • リストが更新されたときに、「誰が残っていて、誰がいなくなったか」を判断するために、keyが使われること
  • 一意なID(idフィールドなど)をkeyにするのが最も安全で基本的な方法であること
  • indexをkeyにすることが、並び替え・削除・追加の場面で問題を生む理由を具体的にイメージしたこと
  • ユーザー一覧の実装を通して、map()+オブジェクト配列+JSX+key+State更新が1本の流れとしてつながる感覚を確認したこと

が大きなポイントになります。

この段階まで来ると、

  • 「keyは警告を消すためのもの」ではなく、
  • 「Reactがリストを正しく理解するための重要な仕組み」

として捉えられるようになっているはずです。 中盤以降では、さらに複雑なリストやフォーム付きのリストを題材に、 keyの重要性をより実践的な形で深めていきます。


DAY 24:keyを理解する 中盤 ― 「動くUI」とkeyの関係を体で理解する

DAY 24の中盤では、前半で学んだ keyの役割 を、 もう一歩「動くUI」の視点から深く理解していきます。

ここからのゴールは、

  • 「なぜkeyが必要なのか」を、実際の挙動からイメージできるようになること
  • 「IDをkeyにするべき理由」と「indexをkeyにする危険性」を、感覚として掴むこと
  • ユーザー一覧を題材に、keyを意識したリスト設計ができるようになること

です。

Reactは「前回」と「今回」を比べている

差分更新という考え方を押さえる

まず、keyの話を深めるために、 Reactが画面をどう更新しているかをイメージしておきます。

Reactは、毎回ゼロからDOMを作り直しているわけではなく、

「前回の仮想DOM」と「今回の仮想DOM」を比べて、差分だけを実際のDOMに反映する」

という仕組みで動いています。

リスト表示の場合、この「差分を取る」処理が少し難しくなります。

例えば、ユーザー一覧が次のような状態だとします。

const usersBefore = [
  { id: 1, name: "太郎" },
  { id: 2, name: "花子" },
  { id: 3, name: "健" },
];
JSX

削除や追加が行われて、次のようになったとします。

const usersAfter = [
  { id: 1, name: "太郎" },
  { id: 3, name: "健" },
  { id: 4, name: "新しいユーザー" },
];
JSX

Reactはここで、

  • 「太郎」は前回も今回もいる
  • 「花子」はいなくなった
  • 「健」は前回も今回もいる
  • 「新しいユーザー」が追加された

ということを判断したいわけです。

このときに使われるのが key です。

keyとは「ReactにとってのID」である

データのIDと、ReactのIDを一致させる

ユーザー一覧の例で考えると、 各ユーザーにはすでに「id」というフィールドがあります。

const users = [
  { id: 1, name: "太郎", email: "taro@example.com" },
  { id: 2, name: "花子", email: "hanako@example.com" },
  { id: 3, name: "健", email: "ken@example.com" },
];
JSX

この id は、

  • データベースやAPIが「ユーザーを一意に識別するため」に持っているID

です。

React側でも、同じように「各要素を一意に識別するためのID」が必要になります。 それが key です。

つまり、

「データのidを、そのままReactのkeyとして使う」

というのが、一番自然で安全な形になります。

{users.map((user) => (
  <li key={user.id}>{user.name}</li>
))}
JSX

ここでの key={user.id} は、

  • 「この <li> は、idが1のユーザーに対応している」
  • 「この <li> は、idが2のユーザーに対応している」

という情報をReactに伝えています。

indexをkeyにしたときに起きる「ズレ」をイメージする

並びが変わると、indexも変わる

indexをkeyにした場合の問題を、もう少し具体的にイメージしてみます。

{users.map((user, index) => (
  <li key={index}>{user.name}</li>
))}
JSX

このとき、Reactは「index」をIDとして扱います。

  • index 0 → 太郎
  • index 1 → 花子
  • index 2 → 健

ここから「太郎」を削除すると、配列はこうなります。

  • index 0 → 花子
  • index 1 → 健

Reactから見ると、

  • 「index 0の要素は前回と違うものになった」
  • 「index 1の要素も前回と違うものになった」

というように見えてしまいます。

本当は「太郎だけが消えた」状態なのに、 「全部入れ替わった」ように誤解される可能性があるわけです。

入力フォーム付きのリストで起きる違和感

この「ズレ」は、入力フォーム付きのリストで特に分かりやすく現れます。

例えば、ユーザー一覧に「メモ入力欄」を付けたとします。

{users.map((user, index) => (
  <li key={index}>
    <p>{user.name}</p>
    <input type="text" placeholder="メモ" />
  </li>
))}
JSX

ユーザーごとにメモを入力している状態で、 先頭のユーザーを削除すると、

  • 入力したメモが「別のユーザーの行」に移ってしまう

というような挙動が起きることがあります。

これは、

  • Reactが「index 0の要素は同じもの」と誤認してしまう
  • 実際には別のユーザーに対応する行になっている

というズレが原因です。

重要ポイント:

  • indexは「配列の並び」に依存する値であり、 並び替え・削除・追加で簡単に変わってしまいます。
  • そのため、「その要素を一意に識別するID」としては不適切です。

IDをkeyにすることで「ズレ」を防ぐ

データのIDは変わらない

一方、user.id をkeyにしている場合はどうでしょうか。

{users.map((user) => (
  <li key={user.id}>
    <p>{user.name}</p>
    <input type="text" placeholder="メモ" />
  </li>
))}
JSX

ここでは、

  • id 1 → 太郎
  • id 2 → 花子
  • id 3 → 健

という対応関係があり、 削除や追加があっても「idそのもの」は変わりません。

先頭のユーザー(id 1)を削除すると、

  • id 2 → 花子
  • id 3 → 健

という状態になりますが、

Reactは、

  • 「id 1の要素が消えた」
  • 「id 2と3の要素は前回と同じものだ」

と認識できます。

その結果、

  • 入力フォームやチェックボックスの状態が、正しいユーザーの行に残る
  • アニメーションやトランジションも、意図した要素に対して適用される

という「自然な挙動」が保たれます。

重要ポイント:

  • データのIDは「そのデータを一意に識別するための値」であり、 並び替えや削除・追加があっても変わりません。
  • だからこそ、Reactのkeyとして使うのに適しています。

実践:ユーザー一覧でkeyの選び方を意識する

正しい形:IDをkeyにしたユーザー一覧

ここで、IDをkeyにしたユーザー一覧をもう一度整理しておきます。

import { useState } from "react";

function UserList() {
  const [users, setUsers] = useState([
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
    { id: 3, name: "健", email: "ken@example.com" },
  ]);

  const handleDelete = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー一覧</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <p>名前:{user.name}</p>
            <p>メール:{user.email}</p>
            <button onClick={() => handleDelete(user.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;
JSX

このコードでは、

  • users は「ユーザー情報のオブジェクト」が入った配列
  • map() で各ユーザーをJSXに変換
  • key={user.id} で、各ユーザーを一意に識別
  • 削除ボタンで配列を更新し、Reactがkeyを使って差分を正しく計算

という流れになっています。

indexをkeyにした場合との違いを意識する

あえて、indexをkeyにした場合も並べておきます。

{users.map((user, index) => (
  <li key={index}>
    <p>名前:{user.name}</p>
    <p>メール:{user.email}</p>
    <button onClick={() => handleDelete(user.id)}>削除</button>
  </li>
))}
JSX

この形は、

  • 「見た目は動く」
  • 「警告も消える」

かもしれませんが、

  • 並び替えや削除・追加が行われたときに、 Reactが「どの要素がどのユーザーに対応しているか」を誤認する可能性があります。

ここでの大事な感覚:

  • 「indexをkeyにすると、Reactの視点と自分の視点がズレる」
  • 「IDをkeyにすると、Reactの視点と自分の視点が一致する」

という違いを意識できると、keyの選び方に迷いがなくなります。

練習用テンプレート:ユーザー一覧+メモ入力付き

最後に、DAY 24中盤の内容をまとめた練習用テンプレートとして、 「ユーザー一覧+メモ入力付き」のコンポーネントを示します。

import { useState } from "react";

function UserListWithNotes() {
  const [users, setUsers] = useState([
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
    { id: 3, name: "健", email: "ken@example.com" },
  ]);

  const handleDelete = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー一覧(メモ入力付き)</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <p>名前:{user.name}</p>
            <p>メール:{user.email}</p>
            <input type="text" placeholder="このユーザーに関するメモ" />
            <button onClick={() => handleDelete(user.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserListWithNotes;
JSX

このテンプレートは、

  • IDをkeyにした場合の「自然な挙動」を体感するための題材になります。
  • 実際に削除を試してみると、
    • 入力したメモが「別のユーザーの行」に移動したりしない
    • 削除した行だけが消え、残りの行はそのまま

ということが確認できます。

DAY 24 中盤のまとめ

DAY 24の中盤では、

  • Reactが「前回」と「今回」の仮想DOMを比べて差分更新していることを前提に、keyの役割を捉え直したこと
  • keyが「ReactにとってのID」であり、データのIDと一致させることで、視点のズレを防げること
  • indexをkeyにした場合に、並び替え・削除・追加で「同じ要素なのに別物として扱われる」ズレが起きることを具体的にイメージしたこと
  • IDをkeyにすることで、入力フォームやチェックボックスを含むリストでも、自然な挙動が保たれること
  • ユーザー一覧+メモ入力付きのテンプレートを通して、 「IDをkeyにするべき理由」を実際のUIの動きとして想像できるようになったこと

が大きなポイントになります。

この段階まで来ると、

  • 「keyはReactのためのID」
  • 「indexではなく、一意なIDを使う」

という原則が、単なる知識ではなく、 「動くUIを設計するための感覚」として自分の中に根付き始めているはずです。


DAY 24:keyを理解する 後半 ― 「実務レベルで通用するkeyの感覚」を仕上げる

DAY 24の後半では、これまで学んできた keyの考え方 を、 「実務レベルでもそのまま通用する」ところまで仕上げていきます。

ここからのゴールは、

  • keyとは / なぜ必要か / IDを使用する / indexをkeyにする問題 を、迷いなく説明できるようになること
  • 「ユーザー一覧」を題材に、追加・削除・編集が絡む場面でも、正しいkey設計ができるようになること
  • 「このリストは安全に動く」と自信を持って言えるコードを書けるようになること

です。

keyとは何かを「一言で説明できる」ようにする

Reactにとっての「その要素のID」です

後半の最初に、改めて key を一言でまとめておきます。

keyとは、Reactがリストの各要素を識別するために使う「その要素のID」です。

ここで大事なのは、

  • 「人間にとってのID(ユーザーIDなど)」
  • 「ReactにとってのID(key)」

一致させる という感覚です。

ユーザー一覧なら、

  • データ側のID → user.id
  • React側のID → key={user.id}

という形で、1対1に対応させます。

この「IDの一致」が、後半で扱う「追加・削除・編集が絡むリスト」を安定して動かすための土台になります。

なぜkeyが必要かを「差分更新」の視点で整理する

Reactは「前回」と「今回」を比べている

Reactは、毎回ゼロからDOMを作り直しているわけではなく、

「前回の仮想DOM」と「今回の仮想DOM」を比べて、差分だけを実際のDOMに反映する」

という仕組みで動いています。

リスト表示の場合、この差分計算をするために、

  • 「この要素は前回もあったものか」
  • 「この要素は新しく追加されたものか」
  • 「この要素は削除されたものか」

を判断する必要があります。

そのときに使われるのが key です。

ユーザー一覧でイメージする

例えば、次のようなユーザー一覧があるとします。

const usersBefore = [
  { id: 1, name: "太郎" },
  { id: 2, name: "花子" },
  { id: 3, name: "健" },
];
JSX

削除や追加が行われて、次のようになったとします。

const usersAfter = [
  { id: 1, name: "太郎" },
  { id: 3, name: "健" },
  { id: 4, name: "新しいユーザー" },
];
JSX

Reactはここで、

  • 「id 1の要素は前回も今回もある」
  • 「id 2の要素はなくなった」
  • 「id 3の要素は前回も今回もある」
  • 「id 4の要素が新しく追加された」

ということを判断したいわけです。

この「誰が残っていて、誰がいなくなったか」を判断するために、 keyが「ReactにとってのID」として使われます。

IDを使用する ― 「データのID」と「Reactのkey」を揃える

ユーザー一覧での基本パターン

ユーザー一覧を例に、IDをkeyに使う基本パターンを整理します。

import { useState } from "react";

function UserList() {
  const [users, setUsers] = useState([
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
    { id: 3, name: "健", email: "ken@example.com" },
  ]);

  const handleDelete = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー一覧</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <p>名前:{user.name}</p>
            <p>メール:{user.email}</p>
            <button onClick={() => handleDelete(user.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;
JSX

ここでのポイントは、

  • users は「ユーザー情報のオブジェクト」が入った配列
  • map() で各ユーザーをJSXに変換
  • key={user.id} で、各ユーザーを一意に識別
  • 削除ボタンで配列を更新し、Reactがkeyを使って差分を正しく計算

という流れになっていることです。

重要ポイント:

  • データ側のID(user.id)とReact側のID(key)を一致させることで、 「この要素はこのユーザーに対応している」という関係が崩れません。
  • これが、追加・削除・編集が絡むリストを安定して動かすための基本です。

indexをkeyにする問題を「実務的な違和感」として捉える

見た目は動くが、内部的には不安定

indexをkeyにした場合の問題は、 「見た目は動いているように見えるが、内部的には不安定」という点にあります。

{users.map((user, index) => (
  <li key={index}>
    <p>名前:{user.name}</p>
    <p>メール:{user.email}</p>
    <button onClick={() => handleDelete(user.id)}>削除</button>
  </li>
))}
JSX

このコードは一見動きますし、Reactの警告も消えます。 しかし、並び替え・削除・追加が行われたときに、

  • 「同じユーザーなのに、別の要素として扱われる」
  • 「別のユーザーなのに、同じ要素として扱われる」

というズレが起きる可能性があります。

入力フォーム付きのユーザー一覧での違和感

特に分かりやすいのが、入力フォーム付きのユーザー一覧です。

{users.map((user, index) => (
  <li key={index}>
    <p>名前:{user.name}</p>
    <p>メール:{user.email}</p>
    <input type="text" placeholder="このユーザーに関するメモ" />
    <button onClick={() => handleDelete(user.id)}>削除</button>
  </li>
))}
JSX

この状態で、

  • 各ユーザーにメモを入力する
  • 先頭のユーザーを削除する

という操作をすると、

  • 入力したメモが「別のユーザーの行」に移ってしまう

というような挙動が起きることがあります。

これは、

  • Reactが「index 0の要素は同じもの」と誤認してしまう
  • 実際には別のユーザーに対応する行になっている

というズレが原因です。

重要ポイント:

  • indexは「配列の並び」に依存する値であり、 並び替え・削除・追加で簡単に変わってしまいます。
  • そのため、「その要素を一意に識別するID」としては不適切です。

実践:ユーザー一覧を作成しながらkeyの設計を仕上げる

追加・削除・編集ができるユーザー一覧

ここで、DAY 24後半の仕上げとして、 「追加・削除・編集」ができるユーザー一覧を作ってみます。

import { useState } from "react";

function UserItem({ user, onDelete, onChangeName }) {
  return (
    <li>
      <input
        type="text"
        value={user.name}
        onChange={(e) => onChangeName(user.id, e.target.value)}
      />
      <span style={{ marginLeft: "8px", marginRight: "8px" }}>
        ({user.email})
      </span>
      <button onClick={() => onDelete(user.id)}>削除</button>
    </li>
  );
}

function UserList() {
  const [users, setUsers] = useState([
    { id: 1, name: "太郎", email: "taro@example.com" },
    { id: 2, name: "花子", email: "hanako@example.com" },
  ]);

  const [nextId, setNextId] = useState(3);

  const handleAddUser = () => {
    const newUser = {
      id: nextId,
      name: `ユーザー${nextId}`,
      email: `user${nextId}@example.com`,
    };
    setUsers((prevUsers) => [...prevUsers, newUser]);
    setNextId((prevId) => prevId + 1);
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  const handleChangeUserName = (id, newName) => {
    setUsers((prevUsers) =>
      prevUsers.map((user) =>
        user.id === id ? { ...user, name: newName } : user
      )
    );
  };

  return (
    <div>
      <h2>ユーザー一覧(追加・削除・編集付き)</h2>
      <button onClick={handleAddUser}>ユーザーを追加</button>
      <ul style={{ marginTop: "8px" }}>
        {users.map((user) => (
          <UserItem
            key={user.id}
            user={user}
            onDelete={handleDeleteUser}
            onChangeName={handleChangeUserName}
          />
        ))}
      </ul>
    </div>
  );
}

export default UserList;
JSX

このコードで確認できること

  • keyとは
    • key={user.id} が「ReactにとってのID」として機能しています。
  • なぜ必要か
    • 追加・削除・編集が行われても、 「idが同じユーザーに対応する行」は同じ要素として扱われます。
  • IDを使用する
    • データ側のID(user.id)とReact側のID(key)が一致しているため、 入力内容や編集状態が正しいユーザーに紐づいたまま保たれます。
  • indexをkeyにする問題
    • もしここで key={index} にしてしまうと、 削除や追加のたびに「どの行がどのユーザーに対応しているか」がズレる可能性があります。

重要ポイント:

  • 実際に「追加・削除・編集」が絡むリストを考えると、 indexをkeyにする危険性がよりはっきり見えてきます。
  • 一意なIDをkeyにすることで、 「この行はこのユーザーに対応している」という関係が、操作をまたいでも崩れません。

DAY 24 後半のまとめ

DAY 24の後半では、

  • keyとは何か を「ReactにとってのID」として一言で説明できるようにしたこと
  • なぜ必要か を、差分更新の仕組みとユーザー一覧の例から具体的にイメージしたこと
  • IDを使用する ことで、データ側のIDとReact側のIDを一致させ、「誰が誰なのか」がブレない状態を作れること
  • indexをkeyにする問題 を、入力フォーム付きのリストや追加・削除・編集が絡む場面での違和感として捉え直したこと
  • 実践として「ユーザー一覧(追加・削除・編集付き)」を実装し、 「一意なIDをkeyにすることが、動くUIを安定させるための必須条件である」という感覚をコードレベルで確認したこと

が大きなポイントになります。

ここまで来ると、

  • 「keyは警告を消すためのもの」ではなく、
  • 「Reactがリストを正しく理解し、動くUIを安定させるための中核的な仕組み」

として、しっかり自分の中に根付いているはずです。

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