Reactのkeyとは、配列から作った要素の1つ1つを見分けるための目印で、mapの中でいちばん外側に返す要素に key={item.id} のように付けます。 Reactは再レンダリングのとき、keyが同じものを「前回と同じ要素」とみなして状態やDOMを引き継ぎます。そのため、keyにはデータごとに決まっていて変わらない値(IDなど)を使います。

この記事では、警告「Each child in a list should have a unique "key" prop.」の原因別の直し方、indexをkeyにしたときに入力欄の値がずれる様子の再現、IDがないデータへの対処、indexで問題ない条件をまとめます。警告文と実行結果は、React 19.3.0 で実際に動かして取得したものです。

key警告の原因別・直し方の早見表

原因 起きる書き方 出る警告 直し方
付け忘れ items.map((t) => <li>{t.title}</li>) Each child in a list should have a unique "key" prop. <li key={t.id}> を付ける
付ける位置の誤り mapの中は <Row task={t} />、keyは Row の内側の li に付けた 同上 mapが直接返す <Row key={t.id} ... /> に付ける
Fragmentの省略記法 mapの中で <>...</> を返している 同上 <Fragment key={t.id}> と書く
IDが undefined key={t.id} と書いたがデータに id がない 同上 データに id を持たせる(後述の採番)
重複 同じ id の要素が2つある Encountered two children with the same key 重複しない値にする。複数の値を組み合わせる
スプレッドに key が入っている <li {...props}> の props に key を含めた A props object containing a "key" prop is being spread into JSX key だけ取り出して key={...} と直接書く
描画中に生成 key={Math.random()} 警告は出ない 追加時や読み込み時に1回だけ採番する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「React mapのkeyの書き方|警告の消し方とindexを避ける理由」

この記事の確認環境

項目 バージョン
React / react-dom 19.3.0(開発ビルド)
TypeScript 7.0.2
実行 Node.js 22.22.2、jsdom 30.1.1、JSXの変換は esbuild 0.28.2

リストの動きは jsdom 上で createRoot を使って描画し、ボタンのクリックと入力欄の値をスクリプトから操作して確かめました。実際のブラウザでの操作確認は未実行です。警告文は console.error に出力された文面をそのまま載せています。

基本の書き方:mapの外側の要素にkeyを付ける

配列をリスト表示するときの完成形です。

type Task = { id: string; title: string };

const tasks: Task[] = [
  { id: "t1", title: "見積もり" },
  { id: "t2", title: "発注" },
  { id: "t3", title: "検品" },
];

export function TaskList() {
  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  );
}
<ul><li>見積もり</li><li>発注</li><li>検品</li></ul>

keyのルールは、React公式ドキュメントの「Rendering Lists」に2つ書かれています(今回ページを開いて確認しました)。

ルール 意味
兄弟の間で重複しない 同じ配列の中で一意であればよい。別の配列となら同じ値でも構わない
変わらない 描画のたびに作り直さない。データに結び付いた値を使う

別々の配列で同じkeyを使っても警告が出ないことは、同じ配列を ul と ol の2か所で描画して確かめました。

なお、keyは出力されるHTMLには現れません。 Reactが内部で使うだけの情報で、子コンポーネントが props.key として読むこともできません。子でも同じ値を使いたいときは、<Row key={task.id} id={task.id} /> のように別の名前でも渡します。

警告文の全文と読み方

keyを付けずに map した場合、開発ビルドのコンソールに次の警告が出ます。

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

Check the render method of `ListA`. See https://react.dev/link/warning-keys for more information.

読むところは2行目です。Check the render method of のあとに、map を書いているコンポーネント名が出ます。まずそのコンポーネントの中の map を探すと、直す場所にたどり着けます。

この警告は表示を止めるものではなく、画面はそのまま描画されます。ただ、あとで説明するように、並べ替えや削除をしたときに状態がずれる原因になるため、出たら直しておくのが確実です。

警告は同じ種類の親要素につき1回しか出なかった

確認中に気付いた点があります。1つのページの中に「keyのない ul のリスト」を複数置いたところ、警告が出たのは最初の1つだけでした。親要素が dl のリストは別に警告が出ています。

順番 親要素 keyの状態 警告
1つ目 ul 付け忘れ 出た
2つ目 ul 位置の誤り 出なかった
3つ目 dl Fragmentの省略記法 出た
4つ目 ul id が undefined 出なかった

2つ目と4つ目を単独で動かすと、どちらも警告が出ました。つまり、1か所直して警告が消えたように見えても、次のリロードで別の場所の警告が出てくることがあります。 警告を1つ直したら、もう一度コンソールを確かめる習慣にすると取りこぼしがありません(React 19.3.0 の開発ビルドでの観察結果です)。

原因別の直し方

原因1:keyの付け忘れ

// 警告が出る
function ListA() {
  return <ul>{tasks.map((t) => <li>{t.title}</li>)}</ul>;
}

// 直した形
function ListA() {
  return <ul>{tasks.map((t) => <li key={t.id}>{t.title}</li>)}</ul>;
}

JSXを配列に入れて変数に持った場合も同じです。const items = [<li>a</li>, <li>b</li>] を {items} で描画したときも、同じ警告が出ました。一方、JSXに直接 li を2つ並べて書いたときは、配列ではないので警告は出ません。

原因2:付ける位置の誤り

mapの中で自作コンポーネントを返しているのに、keyをそのコンポーネントの内側の要素に付けているケースです。

// 警告が出る:key が Row の内側にある
function Row({ task }: { task: Task }) {
  return <li key={task.id}>{task.title}</li>;
}
function ListB() {
  return <ul>{tasks.map((t) => <Row task={t} />)}</ul>;
}
Each child in a list should have a unique "key" prop.

Check the render method of `ListB`. See https://react.dev/link/warning-keys for more information.

keyが必要なのは「配列の直接の要素」、つまり map のコールバックが返すいちばん外側の要素です。Row の内側の li は配列の要素ではないので、そこにkeyを書いても役目を果たしません。

// 直した形:map が返す Row に付ける
function Row({ task }: { task: Task }) {
  return <li>{task.title}</li>;
}
function ListB() {
  return <ul>{tasks.map((t) => <Row key={t.id} task={t} />)}</ul>;
}

原因3:keyの重複

const fruits = [
  { id: "a", title: "りんご" },
  { id: "a", title: "みかん" },
  { id: "b", title: "ぶどう" },
];

function ListC() {
  return <ul>{fruits.map((f) => <li key={f.id}>{f.title}</li>)}</ul>;
}
Encountered two children with the same key, `a`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

文面に重複しているkeyの値(ここでは a)が出るので、データのどこが重なっているかを探せます。直し方は原因によって変わります。

重複の理由 直し方
名前やカテゴリなど、重なりうる値をkeyにしていた IDのように一意な値に変える
2つのデータを結合したら id が重なった 出どころを表す文字と id をつないだ値にする(下のコード)
同じデータを二重に追加していた 追加処理を見直して重複を取り除く

2つのデータを結合する場合は、テンプレート文字列で出どころと id をつなぐと一意になります。

<li key={`${item.source}-${item.id}`}>{item.title}</li>

原因4:Fragmentの省略記法

1件のデータから複数の要素(用語と説明など)を返したいとき、<>...</> で包むとkeyを付ける場所がありません。

// 警告が出る
function ListD() {
  return (
    <dl>
      {tasks.map((t) => (
        <>
          <dt>{t.id}</dt>
          <dd>{t.title}</dd>
        </>
      ))}
    </dl>
  );
}

省略記法にはkeyを書けないので、Fragment を import して明示的に書きます。

import { Fragment } from "react";

function ListD() {
  return (
    <dl>
      {tasks.map((t) => (
        <Fragment key={t.id}>
          <dt>{t.id}</dt>
          <dd>{t.title}</dd>
        </Fragment>
      ))}
    </dl>
  );
}
<dl><dt>t1</dt><dd>見積もり</dd><dt>t2</dt><dd>発注</dd><dt>t3</dt><dd>検品</dd></dl>

Fragment は余分なDOM要素を作らないので、出力されるHTMLは省略記法のときと変わりません。

原因5:スプレッドの中にkeyが入っている

propsをオブジェクトにまとめてスプレッドするとき、その中にkeyを含めると次の警告になります。

function ListE() {
  return (
    <ul>
      {tasks.map((t) => {
        const props = { key: t.id, className: "row" };
        return <li {...props}>{t.title}</li>;
      })}
    </ul>
  );
}
A props object containing a "key" prop is being spread into JSX:
  let props = {key: someKey, className: ..., children: ...};
  <li {...props} />
React keys must be passed directly to JSX without using spread:
  let props = {className: ..., children: ...};
  <li key={someKey} {...props} />

警告文に直し方がそのまま書かれています。keyだけは分けて、直接書きます。

const { key, ...rest } = { key: t.id, className: "row" };
return <li key={key} {...rest}>{t.title}</li>;

原因6:keyに渡した値が undefined

key={t.id} と書いていても、データに id がなければkeyは undefined になり、付け忘れと同じ警告が出ます。「書いたのに警告が消えない」ときは、渡している値が本当に入っているかを console.log(tasks) で確かめてみてください。プロパティ名の綴り(id と ID、_id など)の違いもよくある原因です。

keyの役割:Reactは何を見て「同じ要素」と判断するか

keyがなぜ必要なのかを、対応付けの仕組みから整理します。再レンダリングのとき、Reactは前回の要素の並びと今回の並びを見比べて、keyが同じものを同じ要素とみなします。

keyの比較結果 Reactの動き
前回も今回もある 同じ要素として更新する。stateや入力中の値は引き継がれる
今回だけある 新しく作る(マウント)
前回だけある 取り除く(アンマウント)。stateは破棄される

keyがデータに結び付いていれば、並び順が変わっても「この行はさっきのあの行」と正しく追いかけられます。keyが並び順(index)だと、Reactから見えるのは「0番目・1番目・2番目」という位置だけになり、データが入れ替わっても位置で対応付けされます。次の節でその結果を確かめます。

indexをkeyにすると値がずれる:最小コードで再現

各行に入力欄を持つリストで、keyの付け方を切り替えて比べます。入力欄は、値をReactのstateではなくDOM自身が持つ形(非制御)にしています。

import { useState } from "react";

type Row = { id: string; label: string };

const initialRows: Row[] = [
  { id: "a", label: "A" },
  { id: "b", label: "B" },
  { id: "c", label: "C" },
];

function Line({ label }: { label: string }) {
  return (
    <li>
      <span>{label}</span>
      <input />
    </li>
  );
}

export function Demo({ mode }: { mode: "index" | "id" }) {
  const [rows, setRows] = useState(initialRows);
  return (
    <div>
      <button onClick={() => setRows((r) => r.slice(1))}>先頭を削除</button>
      <button onClick={() => setRows((r) => [...r].reverse())}>逆順</button>
      <button onClick={() => setRows((r) => [{ id: "n", label: "N" }, ...r])}>
        先頭に追加
      </button>
      <ul>
        {rows.map((row, i) => (
          <Line key={mode === "index" ? i : row.id} label={row.label} />
        ))}
      </ul>
    </div>
  );
}

3つの入力欄にそれぞれ「Aのメモ」「Bのメモ」「Cのメモ」と入れてから、ボタンを押した結果です。ラベル=[入力欄の値] の形で表示しています。

実行結果:key が index のとき

key=index / 先頭を削除
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: B=[Aのメモ]  C=[Bのメモ]

key=index / 逆順
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: C=[Aのメモ]  B=[Bのメモ]  A=[Cのメモ]

key=index / 先頭に追加
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: N=[Aのメモ]  A=[Bのメモ]  B=[Cのメモ]  C=[]

ラベルは正しく入れ替わっているのに、入力欄の値は元の位置に残っています。 A を削除したのに「Aのメモ」が B の行に付き、消えたのは末尾の「Cのメモ」でした。

実行結果:key が id のとき

key=id / 先頭を削除
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: B=[Bのメモ]  C=[Cのメモ]

key=id / 逆順
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: C=[Cのメモ]  B=[Bのメモ]  A=[Aのメモ]

key=id / 先頭に追加
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: N=[]  A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]

idをkeyにすると、入力欄の値が行と一緒に動きます。

なぜずれるのか

先頭を削除した場合を、Reactの視点で追ってみます。

位置 削除前(key / ラベル) 削除後(key / ラベル) Reactの判断
0番目 0 / A 0 / B key 0 は前回もある → 同じ要素。ラベルだけ B に更新
1番目 1 / B 1 / C key 1 は前回もある → 同じ要素。ラベルだけ C に更新
2番目 2 / C なし key 2 がなくなった → 取り除く

key が index だと、Reactには「末尾の1つが消え、残りはラベルが変わった」ように見えます。input要素そのものは使い回されるため、DOMが持っている入力値は元の位置に残ります。 入力値に限らず、各行のコンポーネントが useState で持っている状態(開閉、チェック、編集中フラグなど)でも同じことが起きます。

ずれるのは次の条件が重なったときです。

  • 行ごとに状態を持っている(非制御の入力欄、行コンポーネント内の useState、フォーカス、アニメーションの途中経過など)
  • 並び順が変わる操作がある(先頭や途中への追加・削除、並べ替え、絞り込み)

indexをkeyにして問題ない条件

indexが常に不向きというわけではありません。次の条件をすべて満たすリストなら、indexをkeyにしても表示は正しく保たれます。

条件 例
並び順が変わらない 並べ替え・絞り込み・途中への追加や削除がない
追加は末尾だけ ログの追記のように後ろへ足すだけ
行が状態を持たない 文字を表示するだけ。入力欄や行ごとの useState がない
安定したIDがデータにない 固定のメニュー項目、文章を行で分割した結果など

また、入力値を親のstateで管理している(制御された入力欄)場合は、値がデータ側にあるため、indexをkeyにしても値はずれません。配列 ["Aのメモ", "Bのメモ", "Cのメモ"] を value に渡す形で先頭を削除したところ、結果は次のとおりでした。

制御された入力欄 + key=index で先頭を削除: Bのメモ / Cのメモ

ただし、この場合でもフォーカスの位置など、DOMが持つ状態は位置に残ります。あとから並べ替え機能が足されることも多いので、IDがあるなら最初からIDを使うのが手間のかからない選択です。

なお、keyを書かなかったときもReactは位置で対応付けるため、動きはindexをkeyにしたときと同じになります。key={i} は警告を消しますが、動きを変えるものではない点を押さえておきましょう。

IDがないデータにkeyを付ける方法

「データにIDがないから index を使う」という場面はよくあります。その場合は、データを作るとき・受け取ったときに1回だけIDを振り、データの一部として持たせます。

どこからIDを持ってくるか

データの出どころ keyにする値
データベースやAPIから取得 レコードの主キーやID
画面上でユーザーが追加した行 追加した時点で crypto.randomUUID() や連番カウンターで採番
IDのない配列を受け取った 受け取った時点で1回だけ採番して保持
重複しないと分かっている値がある その値(コード、スラッグ、メールアドレスなど)。複数の値の組み合わせも可

追加時にcrypto.randomUUIDで採番する

行を足したり消したりできるメモ一覧の完成形です。IDを作るのは「行を追加」ボタンのハンドラーの中、つまりデータを作る瞬間です。

import { useState } from "react";

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

export function MemoList() {
  const [memos, setMemos] = useState<Memo[]>([]);

  // 追加するときに1回だけ採番する
  const add = () =>
    setMemos((prev) => [...prev, { id: crypto.randomUUID(), text: "" }]);

  const remove = (id: string) =>
    setMemos((prev) => prev.filter((m) => m.id !== id));

  const change = (id: string, text: string) =>
    setMemos((prev) => prev.map((m) => (m.id === id ? { ...m, text } : m)));

  return (
    <div>
      <button onClick={add}>行を追加</button>
      <ul>
        {memos.map((m) => (
          <li key={m.id}>
            <input value={m.text} onChange={(e) => change(m.id, e.target.value)} />
            <button onClick={() => remove(m.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

「行を追加」を3回押し、最初の行の「削除」を押して確かめた結果です。

行数: 3
削除後の行数: 2
randomUUID の例: c12c52d8-8a64-4312-a0f5-c79df1905905

削除や更新の対象も、index ではなく id で指定しているところがポイントです。こうしておくと、並べ替えや絞り込みを足しても同じコードのまま動きます。

crypto.randomUUID について、MDNには次の点が書かれています(今回ページを開いて確認しました)。

  • 36文字のバージョン4のUUID文字列を返す
  • 2022年3月以降、主要なブラウザで広く使える
  • 安全なコンテキスト(HTTPS)でのみ使える

HTTPで開いている検証用の環境などで使えない場合は、連番カウンターが手軽な代わりになります。

let nextId = 1;
const add = () =>
  setMemos((prev) => [...prev, { id: String(nextId++), text: "" }]);

受け取ったデータに1回だけ採番する

IDのない配列をまとめて受け取った場合は、stateに入れる時点で採番します。useState の初期化関数は最初の1回しか実行されないので、IDは再レンダリングしても変わりません。

type Raw = { title: string };

function withIds(raws: Raw[]) {
  return raws.map((r) => ({ ...r, id: crypto.randomUUID() }));
}

function Loaded({ raws }: { raws: Raw[] }) {
  const [items] = useState(() => withIds(raws));
  return (
    <ul>
      {items.map((it) => (
        <li key={it.id}>{it.title}</li>
      ))}
    </ul>
  );
}
再レンダリング前後でIDが同じ: true

APIから取得する場合は、取得した結果を state に入れる処理(fetch のあと)で同じように採番します。

描画中にkeyを生成しない理由

key={Math.random()} や key={crypto.randomUUID()} を map の中に直接書くと、警告は出ません。その代わり、再レンダリングのたびにすべてのkeyが変わり、全行が作り直されます。 先ほどの Demo で key を Math.random() に変え、リストの内容を変えずに親を再レンダリングしただけの結果がこちらです。

key=Math.random() / 再レンダリング
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: A=[]  B=[]  C=[]
  新しくマウントされた行: 3

key=id / 再レンダリング
  前: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  後: A=[Aのメモ]  B=[Bのメモ]  C=[Cのメモ]
  新しくマウントされた行: 0

データは何も変わっていないのに、入力した値が全部消え、3行とも新しくマウントされました。Reactから見ると「前回のkeyが1つも残っていない=全部別の要素」だからです。React公式ドキュメントにも、描画中にkeyを生成すると毎回コンポーネントとDOMが作り直され、処理が遅くなり入力内容も消える、と書かれています。

keyの作り方 再レンダリング時 入力値・state
データのID 同じ要素として更新 保たれる
index 位置が同じなら同じ要素 並びが変わるとずれる
描画中に乱数・UUID 毎回すべて作り直し 毎回消える

IDを作る場所は「データを作るところ」、keyに書くのは「作ってあるIDを読むだけ」と分けて考えると迷いません。

絞り込み・並べ替え・入れ子のリストでの書き方

よく使う形を2つ、完成形で載せます。どちらも key にはデータの id を使っているので、条件が変わって並びが変わっても行の対応は保たれます。

filterとsortを通してからmapする

type Product = { id: number; name: string; price: number; inStock: boolean };

const products: Product[] = [
  { id: 101, name: "ノート", price: 120, inStock: true },
  { id: 102, name: "ペン", price: 80, inStock: false },
  { id: 103, name: "付箋", price: 200, inStock: true },
];

export function ProductList({ onlyInStock }: { onlyInStock: boolean }) {
  const visible = products
    .filter((p) => !onlyInStock || p.inStock)
    .toSorted((a, b) => a.price - b.price);

  if (visible.length === 0) return <p>該当する商品はありません</p>;

  return (
    <ul>
      {visible.map((p) => (
        <li key={p.id}>
          {p.name}({p.price}円)
        </li>
      ))}
    </ul>
  );
}

onlyInStock が false のときと true のときの出力です。

<ul><li>ペン(80円)</li><li>ノート(120円)</li><li>付箋(200円)</li></ul>
<ul><li>ノート(120円)</li><li>付箋(200円)</li></ul>

ここで key={i} にしていると、絞り込みを切り替えた瞬間に「0番目」がペンからノートに変わり、前の節で見たずれが起きる形になります。絞り込みや並べ替えがあるリストは、indexが向かない代表例です。なお toSorted は元の配列を変えずに並べ替えた新しい配列を返すメソッドで、propsやstateの配列をそのまま扱えます。

入れ子のリスト

mapの中にmapがある場合は、それぞれの map が返す要素にkeyを付けます。 keyは兄弟の間で一意であればよいので、内側のリストどうしで同じ値(下の例では m1)があっても警告は出ません。

type Group = { id: string; name: string; members: { id: string; name: string }[] };

const groups: Group[] = [
  { id: "g1", name: "開発", members: [{ id: "m1", name: "佐藤" }, { id: "m2", name: "鈴木" }] },
  { id: "g2", name: "総務", members: [{ id: "m1", name: "高橋" }] },
];

export function GroupList() {
  return (
    <div>
      {groups.map((g) => (
        <section key={g.id}>
          <h3>{g.name}</h3>
          <ul>
            {g.members.map((m) => (
              <li key={m.id}>{m.name}</li>
            ))}
          </ul>
        </section>
      ))}
    </div>
  );
}
<div><section><h3>開発</h3><ul><li>佐藤</li><li>鈴木</li></ul></section><section><h3>総務</h3><ul><li>高橋</li></ul></section></div>

keyに何を使うかの判断フロー

ここまでの内容を、選び方の流れにまとめます。

flowchart TD
  A[リストを map で表示する] --> B{データに変わらないIDがある}
  B -- はい --> C[key にその ID を使う]
  B -- いいえ --> D{並び順が変わる または 行が状態を持つ}
  D -- はい --> E[追加時や受け取り時に1回だけ採番して ID を持たせる]
  E --> C
  D -- いいえ --> F[index を key にしてよい]

迷ったら「IDを持たせる」側に倒しておくと、あとから機能が増えても書き換えずに済みます。

keyの応用:値を変えて状態をリセットする

keyはリスト以外でも使えます。keyが変わると、Reactはそのコンポーネントを別物として作り直すので、これを利用して状態をまとめて初期化できます。

function ProfileForm({ userId }: { userId: string }) {
  const [draft, setDraft] = useState("");
  return (
    <p>
      <span>{userId}</span>
      <input value={draft} onChange={(e) => setDraft(e.target.value)} />
    </p>
  );
}

// 表示するユーザーが変わったら、入力途中の内容も初期化したい
<ProfileForm key={userId} userId={userId} />

入力欄に「下書き」と入れた状態で、userId を u1 から u2 に切り替えた結果です。

keyなし: u2 "下書き"
keyあり: u2 ""

keyがないと同じコンポーネントとして扱われ、前のユーザー向けの入力が残ります。keyに userId を付けると、切り替えと同時に入力が空に戻りました。useEffect で state を消す処理を書かなくても済む、覚えておくと便利な使い方です。

keyのチェックリスト

確認すること 目安
keyは map が返すいちばん外側の要素に付いているか 自作コンポーネントを返すなら、そのコンポーネントに付ける
複数要素を返すとき Fragment を明示しているか <Fragment key={...}> と書く
keyの値は兄弟の中で重複しないか 重なりうる値(名前など)を避け、IDを使う
keyの値は再レンダリングしても変わらないか 描画中に乱数やUUIDを作らない
keyに渡している値は undefined になっていないか データに id が入っているか確かめる
index を使うなら条件を満たすか 並び順が変わらない・行が状態を持たない
削除や更新を index ではなく id で指定しているか 並べ替えを足しても同じコードで動く
警告を直したあと、もう一度コンソールを見たか 同じ種類の親要素では1回しか出ないことがある

よくある質問

Q. keyを付けないとどうなりますか? A. 開発ビルドで警告が出ます。表示はされますが、Reactは位置で要素を対応付けるため、indexをkeyにしたときと同じ動きになり、並べ替えや削除で行ごとの状態がずれることがあります。

Q. keyにindexを使うのはいつでも避けるべきですか? A. 並び順が変わらず、行が状態を持たない表示専用のリストなら問題ありません。入力欄やチェックボックスを含むリスト、追加・削除・並べ替えがあるリストでは、データのIDを使います。

Q. IDと index をつないだ値をkeyにするのはどうですか? A. indexが入っているため、並び順が変わるとkeyも変わり、要素が作り直されます。IDが一意ならIDだけで十分です。IDが重複していて一意にしたい場合は、まずデータ側の重複を整理するのが根本的な対応です。

Q. keyは全体で一意である必要がありますか? A. 同じ配列の中(兄弟の間)で一意であれば足ります。別のリストで同じ値を使っても構いません。今回も、同じ配列を2つのリストで描画して警告が出ないことを確かめました。

Q. 子コンポーネントの中で key の値を読めますか? A. 読めません。子の中で props.key を参照したところ値は undefined で、開発ビルドでは次の警告が出ました。同じ値が必要なら、id など別の名前のpropsでも渡します。

K: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)

まとめ

  • keyは map が返すいちばん外側の要素に付ける。自作コンポーネントを返すなら、その内側ではなくコンポーネント自体に付ける
  • 警告の2行目にあるコンポーネント名から、直す map を探せる
  • 複数要素を返すときは <Fragment key={...}>。省略記法にはkeyを書けない
  • indexをkeyにすると、削除・並べ替え・先頭追加で入力値や行のstateが位置に残ってずれる(React 19.3.0で再現)
  • indexで問題ないのは、並び順が変わらず行が状態を持たないリスト
  • IDがないデータは、追加時・受け取り時に1回だけ採番して持たせる。描画中に生成すると毎回全行が作り直される
  • keyを変えるとコンポーネントが作り直される性質は、状態のリセットにも使える

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「React mapのkeyの書き方|警告の消し方とindexを避ける理由」

株式会社RJCでは、Reactのリスト表示とkeyの付け方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?