こんにちは、かつコーチです。
前回は「Reactでよく出るエラーと解決法まとめ」で、代表的なエラーを一通り見てきました。
今回からは、個別のつまずきポイントをひとつずつ深掘りしていきます。
1本目のテーマは、リストを表示するコードを書くとほぼ必ず遭遇する「keyがありません」警告です。
「動くには動くけど、コンソールに赤い警告が出続けている」という状態のまま放置している人も多いのではないでしょうか。
今日はこの警告の意味と、正しい対処法をしっかり理解していきましょう。
「keyがありません」警告とは?
警告メッセージの内容
配列をもとに map でリスト表示を作ると、コンソールにこんな警告が出ることがあります。
function FruitList() {
const fruits = ["りんご", "バナナ", "みかん"];
return (
<ul>
{fruits.map((fruit) => (
<li>{fruit}</li>
))}
</ul>
);
}
// Warning: Each child in a list should have a unique "key" prop.
日本語に訳すと「リストの各子要素には一意なkey propを指定してください」という内容です。
エラーではなく警告なので、画面は問題なく表示されます。
ですが、これを放置すると後々思わぬバグの原因になるので、必ず対処すべき警告です。
なぜkeyが必要なのか(Reactの差分検出の仕組み)
Reactは、状態が変化するたびに「前回の描画結果」と「今回の描画結果」を比較して、変わった部分だけを実際のDOMに反映します。
この仕組みを差分検出(Reconciliation)と呼びます。
配列のようにリスト状の要素を比較するとき、Reactは「どの要素とどの要素が対応しているか」を判断する目印としてkeyを使います。
keyがないと、Reactは「配列の並び順」だけを頼りに前回と今回を対応づけようとします。
その結果、要素の並び替えや削除が起きたときに、本来更新されるべきではない要素まで作り直されてしまうことがあります。
keyはReactに「この要素は前回のどの要素と同じものか」を教えるための目印、とイメージすると理解しやすいです。
基本の対処法:keyに何を指定すればいいか
一意なIDをkeyに使う
もっとも安全なのは、データそのものが持っている一意なID(識別子)をkeyに使う方法です。
type Fruit = {
id: string;
name: string;
};
function FruitList({ fruits }: { fruits: Fruit[] }) {
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit.id}>{fruit.name}</li>
))}
</ul>
);
}
データベースの主キーや、UUIDのように「その要素だけに紐づく値」があれば、それをそのままkeyに指定すれば十分です。
nameのような重複しうる値をkeyにしてしまうと、同じ名前のデータが2つあったときに正しく区別できなくなるので避けましょう。
indexをkeyに使ってはいけない理由
一意なIDがない場合、配列のindex(添字)をkeyに使いたくなりますが、これはおすすめできません。
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
indexは「配列の何番目か」という位置の情報でしかなく、データそのものを識別する値ではありません。
並び替えや先頭への追加が発生すると、同じデータなのにindexがずれてしまい、Reactが「別のデータに変わった」と誤解してしまいます。
つまずきやすいポイント:indexをkeyに使ってハマった話
かつコーチが実際にハマった例
私が実際につまずいたのは、TODOリストにアイテムを削除する機能をつけたときです。
indexをkeyに使っていたところ、リストの途中の項目を削除したときに、削除したはずの項目とは違う項目の入力欄の中身が残ってしまう現象に遭遇しました。
原因は、削除によって後ろの項目のindexがひとつずつ繰り上がり、Reactが「同じkeyの要素だから中身も同じはず」と判断して、入力中の状態(フォームの値)をそのまま使い回してしまったことでした。
❌ Before:indexをkeyに使ってしまう
type Todo = {
id: string;
text: string;
};
function TodoList({ todos, onRemove }: { todos: Todo[]; onRemove: (id: string) => void }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input defaultValue={todo.text} />
<button onClick={() => onRemove(todo.id)}>削除</button>
</li>
))}
</ul>
);
}
このコードでは、真ん中の項目を削除すると、以降の項目のindexがひとつずつずれます。
Reactは「key=1の要素は前回も今回も存在する」としか判断できないため、削除前とは別のデータなのに、inputの中身(入力状態)をそのまま引き継いでしまいます。
✅ After:データが持つ一意なidをkeyに使う
type Todo = {
id: string;
text: string;
};
function TodoList({ todos, onRemove }: { todos: Todo[]; onRemove: (id: string) => void }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input defaultValue={todo.text} />
<button onClick={() => onRemove(todo.id)}>削除</button>
</li>
))}
</ul>
);
}
todo.idをkeyにすることで、並び順が変わっても削除が起きても、Reactは正しく「同じデータ」「別のデータ」を判別できるようになります。
この一件以来、私はリスト表示のコードを書くときは真っ先に「一意なidはどこから取れるか」を確認するようにしています。
応用:一意なIDがないデータへの対処
データ生成時にIDを付与する
APIから返ってくるデータにIDが含まれていない場合は、フロントエンドでデータを受け取った時点でIDを付与してしまうのも有効な手です。
import { useState } from "react";
type Item = {
id: string;
label: string;
};
function useItemsWithId(rawLabels: string[]): Item[] {
const [items] = useState<Item[]>(() =>
rawLabels.map((label) => ({
id: crypto.randomUUID(),
label,
}))
);
return items;
}
crypto.randomUUID()を使って、データを受け取った最初の1回だけIDを生成しておけば、以降はそのIDをkeyとして安定して使い回せます。
useStateの初期化関数の中で生成することで、再レンダリングのたびに新しいIDが振られてしまう事故も防げます。
どうしても静的なリストで並び替えが起きない場合
表示順が固定で、要素の追加・削除・並び替えが一切発生しないリストに限っては、indexをkeyに使ってもすぐに問題は起きません。
ただし、後から機能追加で並び替えや削除が入る可能性は十分にあります。
迷ったときは「将来並び替えが起きても壊れない書き方」を最初から選んでおくと安心です。
まとめ
この記事のポイント
- 「keyがありません」警告は、Reactが差分検出のためにリストの各要素を識別できていない状態を示している
keyにはデータが持つ一意なIDを使うのが基本indexをkeyに使うと、並び替えや削除で意図しないバグにつながることがある- 一意なIDがないデータは、受け取った時点でIDを付与しておくと安全
次に読むべき記事
次回は「無限re-renderが起きる原因と対処法」を解説します。
useEffectまわりでよく遭遇するエラーなので、ぜひ続けて読んでみてください。
→ 次の記事:無限re-renderが起きる原因と対処法
