初級7分で読める

【解決】Each child in a list should have a unique key エラーの修正方法

ReactのEach child in a list should have a unique 'key' propエラーを解説。keyが必要な理由・正しいkeyの選び方・indexをkeyにしてはいけないケースを実例付きで説明します。

コンソールに出るこの警告、最初は「とりあえず動いてるからいいか」と放置しがちだ。

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

でもこれを放置すると、リストの並び替えや更新のとき画面がおかしな挙動をする。エラーではなく「Warning」なのに対処すべき理由がそこにある。

なぜReactはkeyを必要とするのか

Reactは画面を更新するとき、前回の状態と今回の状態を比較して「何が変わったか」を計算する。このとき、リストの各要素を識別する手がかりが必要になる。

keyがない場合、Reactは「順番」で判断するしかない。1番目・2番目・3番目という位置だけを頼りに差分を計算する。

これが問題になるのは、リストの順番が変わるときだ。

変更前: [A, B, C]
変更後: [B, C]  ← Aを削除した

keyがないとReactはこう解釈する。「1番目がAからBに変わり、2番目がBからCに変わり、3番目が消えた」。実際には「1番目が消えただけ」なのに、3つの変更として処理してしまう。

keyがあればこうなる。「key=Bの要素とkey=Cの要素はそのまま。key=Aの要素が消えた」。実態通りの1操作として処理できる。

コンポーネントに状態(入力値など)がある場合、keyなしのリストでは「位置」で状態が紐づくため、要素を削除や並び替えしたときに状態が意図せず別の要素に引き継がれることがある。

基本の修正:ユニークなIDをkeyに使う

// ❌ keyがない
function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map((user) => (
        <li>{user.name}</li>
      ))}
    </ul>
  )
}

// ✅ データが持つユニークなIDをkeyに使う
function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

keyに使うべき値の基準は「そのデータを一意に識別できる、変わらない値」だ。データベースのIDやユーザーIDなど、バックエンドが発行したIDが最適。

フラグメントにkeyを付けたいとき

複数の要素をまとめてレンダリングするとき、短縮記法の <>...</> ではkeyが付けられない。

// ❌ 短縮記法はkeyを付けられない
{items.map((item) => (
  <>
    <dt>{item.term}</dt>
    <dd>{item.desc}</dd>
  </>
))}

// ✅ Fragment の明示的な記法を使う
import { Fragment } from 'react'

{items.map((item) => (
  <Fragment key={item.term}>
    <dt>{item.term}</dt>
    <dd>{item.desc}</dd>
  </Fragment>
))}

indexをkeyにしていいケース・だめなケース

「IDがないからindexを使えばいい」と思いがちだが、これは条件次第だ。

indexを使ってはいけないケース:リストに変更がある場合

// ❌ 並び替えや削除がある場合、indexをkeyにすると状態がずれる
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}

たとえばindex=0のTodoItemがチェックされている状態で、0番目のアイテムを削除すると、元1番目だったアイテムがindex=0になる。Reactはindex=0のコンポーネントが「同じもの」と判断して状態を引き継いでしまう。結果、チェック状態が別のアイテムに移る。

// ✅ IDがある場合は必ずIDを使う
{todos.map((todo) => (
  <TodoItem key={todo.id} todo={todo} />
))}

indexを使ってよいケース:リストが変化しない場合

// ✅ ナビゲーションリンクなど、順番も内容も変わらない静的なリスト
const navLinks = ['ホーム', '記事一覧', 'について']

{navLinks.map((link, index) => (
  <li key={index}>{link}</li>
))}

変わらない・並び替えない・状態を持たない——この3条件が揃えばindexでも問題ない。

よくある間違ったkeyの付け方

// ❌ Math.random()は毎回変わる → レンダリングのたびに全要素が再生成される
<Item key={Math.random()} />

// ❌ 配列のindexと値を組み合わせても、根本の問題は変わらない
<Item key={`item-${index}`} />

// ✅ データが持つ安定したIDを使う
<Item key={item.id} />

// ✅ 単一フィールドでユニークにならない場合は複数を組み合わせる
<Item key={`${item.userId}-${item.date}`} />

keyはpropsとして受け取れない

key はReact内部の管理用であり、コンポーネントのpropsとしては渡ってこない。

// ❌ props.key は常にundefined
function Item({ key, name }: { key: string; name: string }) {
  return <li>{key}: {name}</li>
}

// ✅ 別名のpropsとして渡す
function Item({ id, name }: { id: string; name: string }) {
  return <li>{id}: {name}</li>
}

{items.map((item) => (
  <Item key={item.id} id={item.id} name={item.name} />
))}

このエラーと向き合うための1つの考え方

リストの各要素には、データ側が持つ「本人確認できる番号」をkeyに使う。

位置(index)ではなく、そのデータ自体のアイデンティティをkeyに渡すことで、Reactはリストがどう変化しても正確に差分を計算できる。