エラーの内容
ブラウザのコンソールを開くと、次の警告が表示されます:
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `UserList`. See https://reactjs.org/link/warning-keys for more information.
at li
at UserList
Reactは、配列をmapで反復する際にkeyプロパティを忘れたとき、またはリスト内のキーが実際には一意でないときにこの警告をスローします。
なぜこれが重要なのか
Reactはkeyを使って、レンダリング間でどのアイテムが変更、移動、または削除されたかを追跡します。一意のキーがないと、差分アルゴリズムが推測するしかなくなり、誤った推測をしてしまいます。
具体的なシナリオを見てみましょう:5つのアイテムがあるTodoリストで、2番目のアイテムを削除するとします。インデックスをキーとして使用すると、3番目のアイテムがインデックス1を占めるようになります。Reactはそれを古い2番目のアイテムと同じコンポーネントと見なし、更新をスキップします。結果として、チェックボックスが間違ったアイテムにチェックされていたり、フォームの入力欄に古い値が表示されたりします。
具体的に、不適切なキーが引き起こす問題:
- 並び替えや削除後にコンポーネントの状態が狂う
- 予期しない再レンダリングや更新の見落とし
- リストが変更された後も入力フィールドに古い値が残る
これはクラッシュではなく警告です。しかし長期間無視し続けると、再現が困難に見えるバグを追いかけて何時間も費やすことになります。
よくある原因
- リストアイテムに
keyプロパティが完全に欠けている - リストが並び替えやフィルタリング可能なのに、配列のインデックスをキーとして使用している
- データに重複したIDがある
- キーが間違った要素に配置されている――最も外側の返却要素ではなく、ラッパーのフラグメントに置かれている
ステップごとの修正方法
1. すべてのリストアイテムにkeyを追加する
最も基本的なケース――配列をmapしているのにkeyがまったくない場合:
// ❌ keyが欠けている
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li>{user.name}</li> // ここで警告が発生
))}
</ul>
);
}
// ✅ 安定した一意のIDを使ってkeyを追加する
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. 配列のインデックスをkeyとして使用するのをやめる
インデックスキーは一見無害に見えます。警告も消えます。しかし、ユーザーが並び替え、フィルタリング、削除を行った瞬間に問題が発生します:
// ❌ インデックスキー――並び替え・削除で壊れる
{items.map((item, index) => (
<TodoItem key={index} item={item} />
))}
// ✅ データから安定したIDを使用する
{items.map(item => (
<TodoItem key={item.id} item={item} />
))}
インデックスキーが安全なのは、本当に静的なリスト――並び替え、フィルタリング、縮小が絶対に起きないもの――に限られます。それ以外はすべて本物のIDが必要です。
IDはデータが作成されるときに生成しましょう(例:crypto.randomUUID()やnanoid()を使用)。レンダリング中に生成してはいけません。詳しくはステップ5で説明します。
3. フラグメントへのkey
アイテムごとに複数の要素を返す場合は、keyを持つFragmentが必要です。省略記法の<></>はpropsを受け付けないため、明示的な<Fragment>インポートを使用しなければなりません:
import { Fragment } from 'react';
// ❌ 省略記法のフラグメントはkeyを受け取れない
{items.map(item => (
<>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</>
))}
// ✅ keyを持つ明示的なFragment
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
))}
4. ネストしたリスト
リストのすべての階層にkeyが必要です――最も外側だけではありません:
// ❌ 内側のリストアイテムにkeyがない
{categories.map(cat => (
<div key={cat.id}>
<h3>{cat.name}</h3>
<ul>
{cat.items.map(item => (
<li>{item.label}</li> // ← ここでも警告が出る
))}
</ul>
</div>
))}
// ✅ すべてのmap対象要素にkey
{categories.map(cat => (
<div key={cat.id}>
<h3>{cat.name}</h3>
<ul>
{cat.items.map(item => (
<li key={item.id}>{item.label}</li>
))}
</ul>
</div>
))}
5. 本当にIDがない場合
本当に静的なリスト――決して並び替えず、繰り返しもない、固定された言語名やステータスラベル、設定オプションのセットなど――であれば、値そのものを有効なキーとして使えます:
const SUPPORTED_LANGS = ['JavaScript', 'TypeScript', 'Python', 'Go'];
{SUPPORTED_LANGS.map(lang => (
<li key={lang}>{lang}</li>
))}
動的なデータは話が別です。データがコンポーネントに渡される前の源泉でIDを付与しましょう。nanoidを使った簡単な方法:
import { nanoid } from 'nanoid';
const items = rawData.map(item => ({ ...item, id: nanoid() }));
// あとは item.id をkeyとして使用する
これはコンポーネントの外で一度だけ行いましょう。レンダリング中に.map()内でnanoid()やMath.random()を呼び出してはいけません――レンダリングのたびに新しいキーが生成され、Reactがすべてのアイテムをアンマウントして再マウントするよう強制されます。
修正の確認
- Chrome DevToolsを開く → Consoleタブを選択
- ページをリロードする――keyの警告が消えているはず
- リストをストレステストする:アイテムを追加・削除し、可能であれば並び替えを行う
- 各操作の後、リストアイテム内の入力フィールドが値を正しく保持していることを確認する
- React DevTools(Componentsタブ)でリストアイテムを検査する――keyは内部的なものでpropsとしては表示されないが、コンソールの警告は出なくなる
クイックチェックリスト
- すべての
.map()コールバックがkeyプロパティを持つ要素を返している - keyは配列のインデックスではなく、安定したデータIDから取得している(リストが本当に静的で変更されない場合を除く)
- keyは兄弟要素の中で一意であればよい――グローバルに一意である必要はない
- keyを持つフラグメントは
<Fragment key={...}>を使用し、<></>は使わない - ネストしたリストはすべての階層にkeyがある
- keyはレンダリング中に生成しない――
.map()内でのMath.random()、Date.now()、インラインのnanoid()は禁止

