React key prop warningの原因と解決方法|初心者向け完全ガイド
Reactを使用していると、ブラウザのコンソールに以下のようなwarningメッセージが表示されることがあります:
「Warning: Each child in a list should have a unique “key” prop.」
このエラーメッセージに困ったことはありませんか?本記事では、このReact key prop warningの原因から実践的な解決方法まで、初心者向けに詳しく解説します。
目次
- key propが必要な理由
- warningが発生する原因
- 解決手順
- 実践的なコード例
- よくある間違い
- まとめ
key propが必要な理由|原因の詳細説明
Reactのkey propについて理解するには、まずReactの仮想DOMの仕組みを知る必要があります。
Reactの仮想DOMと再レンダリング
Reactは効率的にUI更新を行うため、「仮想DOM」という概念を使用しています。コンポーネントの状態が変わると、Reactは新しい仮想DOMツリーを生成し、前回のバージョンと比較します。この比較プロセスを「Reconciliation(差分抽出)」と呼びます。
リストを含むコンポーネントで状態が更新される場合、Reactは各要素を識別する必要があります。このときに使用されるのが「key prop」です。keyがないと、Reactはリストの位置(インデックス)に基づいて要素を認識してしまい、予期しない問題が発生する可能性があります。
具体的な問題例
例えば、ユーザーのリストがあり、そこにチェックボックスの状態が保持されているとします。keyを正しく設定していないと、リストの並び順が変わったときに、チェックボックスの状態が正しく対応されないという問題が起きます。
つまり、key propはReactが「この要素は前回のレンダリング時のこの要素と同じものです」と認識するための重要な識別子なのです。
key prop warningが発生する原因
原因1:keyを指定していない
最も一般的な原因は、リストを描画する際にkey propを全く指定していないケースです。
原因2:keyとしてインデックスを使用している
一見正しく見えても、配列のインデックスをkeyとして使用するのは問題があります。リストの項目が追加・削除・並び替えされると、インデックスが変わってしまい、keyの意味がなくなります。
原因3:keyが重複している
複数の要素が同じkeyを持つことも問題です。Reactは各keyがユニークであることを期待しています。
原因4:keyが動的に生成されている
毎回のレンダリング時にkeyが異なる値になる場合、warning が発生します。例えば、乱数やタイムスタンプをkeyとして使用するなどです。
key prop warningの解決手順
ステップ1:問題のあるコードを特定する
まず、ブラウザの開発者ツール(Developer Tools)でコンソールを確認します。warningメッセージには通常、問題が発生しているコンポーネント名が表示されます。
ステップ2:リストを描画しているコードを確認
map()関数を使用してリストを描画している部分を探します。
ステップ3:適切なkeyを決定する
リストの各要素に一意の識別子があるか確認します。理想的には、データベースのIDや、ユーザーのメールアドレスなど、変わらないユニークな値を使用します。
ステップ4:key propを追加する
map()関数で返すJSX要素にkey propを追加します。
ステップ5:動作確認
コンソールのwarningが消えたか、またリストの操作(追加・削除・並び替え)が正しく動作しているか確認します。
実践的なコード例
例1:基本的なリスト表示(問題のあるコード)
// ❌ これは間違い:keyがない\nfunction UserList() {\n const users = [\n { id: 1, name: '太郎' },\n { id: 2, name: '次郎' },\n { id: 3, name: '三郎' }\n ];\n\n return (\n <ul>\n {users.map((user) => (\n <li>{user.name}</li>\n ))}\n </ul>\n );\n}\n
このコードを実行すると、コンソールに警告が表示されます:「Warning: Each child in a list should have a unique “key” prop.」
例2:インデックスをkeyとして使用(悪い実装)
// ⚠️ これは避けるべき:indexをkeyとして使用\nfunction UserList() {\n const users = [\n { id: 1, name: '太郎' },\n { id: 2, name: '次郎' },\n { id: 3, name: '三郎' }\n ];\n\n return (\n <ul>\n {users.map((user, index) => (\n <li key={index}>{user.name}</li>\n ))}\n </ul>\n );\n}\n
確かにwarningは消えますが、リストの順序が変わると問題が発生します。以下のコード例でその問題を見てみましょう。
例3:ユニークなIDをkeyとして使用(正しい実装)
// ✅ これが正しい:ユニークなIDをkeyとして使用\nfunction UserList() {\n const users = [\n { id: 1, name: '太郎' },\n { id: 2, name: '次郎' },\n { id: 3, name: '三郎' }\n ];\n\n return (\n <ul>\n {users.map((user) => (\n <li key={user.id}>{user.name}</li>\n ))}\n </ul>\n );\n}\n
このコードは正しく機能し、warningも表示されません。user.idはデータベースから取得されたもので、変わることのないユニークな値です。
例4:フォーム要素を含むリスト(インデックスキーの問題を示す例)
import { useState } from 'react';\n\n// ❌ インデックスをkeyとして使用した場合の問題\nfunction TodoListWithIndexKey() {\n const [todos, setTodos] = useState([\n { id: 1, text: 'タスク1' },\n { id: 2, text: 'タスク2' },\n { id: 3, text: 'タスク3' }\n ]);\n\n const addTodo = () => {\n const newTodo = {\n id: Math.max(...todos.map(t => t.id)) + 1,\n text: `タスク${todos.length + 1}`\n };\n setTodos([newTodo, ...todos]); // 先頭に追加\n };\n\n return (\n <div>\n <button onClick={addTodo}>追加</button>\n <ul>\n {todos.map((todo, index) => (\n <li key={index}>\n <input type=\"checkbox\" />\n {todo.text}\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\nexport default TodoListWithIndexKey;\n
このコードの問題は、新しいタスクを先頭に追加すると、既存のチェックボックス状態が新しいタスクに移ってしまうことです。
例5:上記の問題を修正したコード(正しい実装)
import { useState } from 'react';\n\n// ✅ ユニークなIDをkeyとして使用(正しい実装)\nfunction TodoListWithProperKey() {\n const [todos, setTodos] = useState([\n { id: 1, text: 'タスク1' },\n { id: 2, text: 'タスク2' },\n { id: 3, text: 'タスク3' }\n ]);\n\n const addTodo = () => {\n const newTodo = {\n id: Math.max(...todos.map(t => t.id), 0) + 1,\n text: `タスク${todos.length + 1}`\n };\n setTodos([newTodo, ...todos]); // 先頭に追加\n };\n\n return (\n <div>\n <button onClick={addTodo}>追加</button>\n <ul>\n {todos.map((todo) => (\n <li key={todo.id}>\n <input type=\"checkbox\" />\n {todo.text}\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\nexport default TodoListWithProperKey;\n
この修正版では、各タスクのチェックボックス状態が正しく保持されます。新しいタスクを追加しても、既存のタスクの状態には影響しません。
例6:APIから取得したデータを表示する場合
import { useState, useEffect } from 'react';\n\nfunction PostList() {\n const [posts, setPosts] = useState([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n // APIからデータ取得\n fetch('https://api.example.com/posts')\n .then(response => response.json())\n .then(data => {\n setPosts(data);\n setLoading(false);\n });\n }, []);\n\n if (loading) return <p>読み込み中...</p>;\n\n return (\n <div>\n {posts.map((post) => (\n <div key={post.id} className=\"post\">\n <h2>{post.title}</h2>\n <p>{post.body}</p>\n <small>投稿者ID: {post.userId}</small>\n </div>\n ))}\n </div>\n );\n}\n\nexport default PostList;\n
APIから取得したデータには通常、一意のIDが含まれています。このIDをkeyとして使用するのが最適な方法です。
例7:複数のリストが存在する場合
function CommentSection() {\n const comments = [\n { id: 101, text: 'いいね' },\n { id: 102, text: 'ありがとう' },\n { id: 103, text: '参考になりました' }\n ];\n\n const replies = [\n { id: 201, text: '返信1' },\n { id: 202, text: '返信2' }\n ];\n\n return (\n <div>\n <section>\n <h3>コメント</h3>\n <ul>\n {comments.map((comment) => (\n <li key={comment.id}>{comment.text}</li>\n ))}\n </ul>\n </section>\n\n <section>\n <h3>返信</h3>\n <ul>\n {replies.map((reply) => (\n <li key={reply.id}>{reply.text}</li>\n ))}\n </ul>\n </section>\n </div>\n );\n}\n\nexport default CommentSection;\n
複数のリストがある場合でも、各リスト内でユニークなIDを使用すれば問題ありません。IDが異なるリスト間で重複していても、同じリスト内でユニークであれば大丈夫です。
よくある間違いと対処法
間違い1:String()やtoString()で動的に生成したkeyを使用
// ❌ 間違い:毎回異なるキーが生成される\n{items.map((item) => (\n <div key={String(Math.random())}>\n {item.name}\n </div>\n))}\n\n// ✅ 正解\n{items.map((item) => (\n <div key={item.id}>\n {item.name}\n </div>\n))}\n
毎回のレンダリングで異なる値が生成されると、Reactはその要素を毎回新しいものとして扱い、意味のあるkeyにはなりません。
間違い2:ネストされたリストで単純なIDを使用
// ⚠️ 危険:親と子でIDが重複する可能性\nfunction NestedList() {\n const categories = [\n {\n id: 1,\n name: 'カテゴリA',\n items: [\n { id: 1, name: '項目1' },\n { id: 2, name: '項目2' }\n ]\n },\n {\n id: 2,\n name: 'カテゴリB',\n items: [\n { id: 1, name: '項目3' },\n { id: 2, name: '項目4' }\n ]\n }\n ];\n\n return (\n <div>\n {categories.map((category) => (\n <div key={category.id}>\n <h3>{category.name}</h3>\n <ul>\n {category.items.map((item) => (\n <li key={item.id}>{item.name}</li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n );\n}\n
ネストされたリストでは、子要素のIDが親によって重複する可能性があります。この場合、親のIDと組み合わせてユニークなキーを作成するのが安全です:
// ✅ 正解:親と子を組み合わせてユニークなキーを作成\n<li key={`${category.id}-${item.id}`}>\n {item.name}\n</li>\n
間違い3:オブジェクトや配列をkeyとして使用
// ❌ 絶対にしてはいけない\n{items.map((item) => (\n <div key={item}> {/* オブジェクトをkeyに */}\n {item.name}\n </div>\n))}\n\n// ✅ 正解:プリミティブ値(文字列または数値)を使用\n{items.map((item) => (\n <div key={item.id}>\n {item.name}\n </div>\n))}\n
Reactのkeyは文字列または数値である必要があります。オブジェクトや配列を直接使用することはできません。
間違い4:keyと異なる値で条件判定を行う
// ⚠️ 予期しない動作の原因になる\n{users.map((user) => (\n <div key={user.id}>\n {user.id === currentUserId ? '現在のユーザー' : user.name}\n </div>\n))}\n
keyとして使用している値と、その要素内で表示や動作の判定に使用する値は一致しているべきです。そうでないと、デバッグが難しくなります。
まとめ
React key prop warningを解決するための重要なポイントをまとめます:
重要なポイント
- keyは必須: リストを描画する際、各要素にkeyを指定することは必須です。
- ユニークであること: 同じリスト内で、すべてのkeyはユニークである必要があります。
- インデックスは避ける: 配列のインデックスをkeyとして使用するのは避けるべきです。特にリストが動的に変わる場合は問題が生じます。
- 永続的な値を使う: データベースのID、ユーザーメール、UUIDなど、変わらないユニークな値を使用します。
- 動的生成を避ける: Math.random()やDate.now()など、毎回異なる値が生成される方法は使用しません。
最適な実装方法
- データにユニークなIDが含まれているか確認する
- リスト項目を描画する際、そのIDをkeyとして指定する
- ブラウザコンソールでwarningが消えたか確認する
- リストの追加・削除・並び替え動作を確認して、正しく機能しているかテストする
これらのポイントを守ることで、Reactアプリケーションはより安定して動作し、予期しない問題を防ぐことができます。
次のステップ
key propの理解を深めたら、以下のトピックについても学ぶことをお勧めします:
- Reactのライフサイクルメソッド
- useEffectフックの正しい使用方法
- パフォーマンス最適化テクニック
- 高度な状態管理パターン
key propは一見シンプルに見えますが、Reactの内部動作を理解するための重要な概念です。正しく使用することで、より堅牢で予測可能なコンポーネントを作成できます。

