React で配列を map して一覧を表示すると、コンソールに「Each child in a list should have a unique “key” prop.」という警告が出た経験はありませんか。これは、リストの各要素に key 属性を付けてください、という React からのお願いです。この記事では、key がそもそも何のためにあるのか、React が内部でどう使っているのか、そしてなぜ配列の index を key にすると不具合が起きるのかを、具体的なコードとともに初心者向けに解説します。
目次
key はなぜ必要なのか
React は、状態が変わるたびに画面を再描画します。このとき毎回すべてを作り直すのではなく、「前回と今回で何が変わったか」を比べて、変わった部分だけを効率よく更新します。ところがリスト(同じ種類の要素が並んだもの)では、要素が増えたのか減ったのか、順番が入れ替わったのかを、見た目だけで判断するのは困難です。
そこで各要素に付けるのが key です。key は「この要素はどれなのか」を React に伝える目印(identity)です。React は key を見て「この key の要素は前回もあった」「この key は新しく増えた」と判断し、必要最小限の更新で済ませます。まずは基本の書き方を見てみましょう。
type Todo = { id: number; text: string };
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map((todo) => (
// 各要素を一意に識別できる id を key に指定する
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
key は map で生成する一番外側の要素に付けます。上の例では <li> です。値には、各データを一意に識別できるもの——多くの場合はデータの id ——を指定します。key は React が内部で使う特別な属性で、子コンポーネントの props としては渡されない点も覚えておきましょう。
index を key にすると何が問題か
map のコールバックは第2引数で配列のインデックス(index)を受け取れるため、つい key={index} と書きたくなります。データに id がないときは特にそうです。しかし、リストの並び替え・先頭への追加・途中の削除が起こると、これが不具合の原因になります。
理由はこうです。index は「今の並び順での位置番号」でしかありません。リストの先頭に要素を追加すると、それまで 0 番だった要素は 1 番に、1 番は 2 番に……と、全員の index がずれます。React から見ると「key=0 の要素の中身が変わった」ように見えてしまい、要素の対応関係を取り違えます。その結果、要素そのものが持っている状態(入力欄の値やチェック状態など)が、別の要素に引き継がれてしまうのです。
// 各行にチェックボックス(要素自身の状態)があるリスト
function BadList({ items }: { items: string[] }) {
return (
<ul>
{items.map((item, index) => (
// index を key にすると、並べ替え・削除で状態がずれる
<li key={index}>
<input type="checkbox" /> {item}
</li>
))}
</ul>
);
}
このリストで、たとえば1番目の項目にチェックを入れてから、先頭に新しい項目を追加したとします。すると index がずれるため、チェックが別の項目に付いたように見える、という現象が起きます。見た目のバグとしては非常に分かりにくく、原因の特定に時間がかかりがちです。
正しくは、データ自体が持つ変わらない ID を key にします。ID があれば、並び順が変わっても「この key はあの要素」という対応が保たれ、状態も正しく付いて回ります。
type Item = { id: string; name: string };
function GoodList({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item) => (
// データが持つ安定した id を key にする
<li key={item.id}>
<input type="checkbox" /> {item.name}
</li>
))}
</ul>
);
}
良い key の選び方
key に向いている値は、次の条件を満たすものです。React は同じ親の中で key が重複していないことを前提にしているため、一意性はとくに重要です。
| 条件 | 説明 |
|---|---|
| 一意である | 同じリスト内で重複しない(データベースの id など) |
| 安定している | 再描画や並べ替えのたびに変わらない |
| そのデータに紐づく | 位置ではなく「そのデータ自身」を表す値である |
データベースの主キーや、外部 API が返す一意な ID が理想的です。どうしても一意な ID がないときは、データを作る段階で crypto.randomUUID() などで ID を振っておくとよいでしょう。描画のたびに新しい値を生成する(例:key={Math.random()})のは絶対に避けてください。毎回 key が変わると、React は「全部が新しい要素」とみなして毎回作り直すため、状態が消え、パフォーマンスも悪化します。
index を key にしてもよい場合
とはいえ、index を key にしても問題ない場面もあります。次の3つをすべて満たすリストなら、index でも実害はほぼありません。並び順が固定で、要素自身が状態を持たないなら、対応がずれる余地がないからです。
- リストの並び順が変わらない(追加・削除・並べ替えをしない)
- 各要素が入力欄やチェック状態などの独自の状態を持たない
- データに使えそうな一意の ID が本当にない
ただし、最初は「変わらないリスト」でも、後から並べ替え機能を足すことはよくあります。迷ったら ID を key にしておくのが安全です。index はあくまで「他に手段がないときの最終手段」と考えておきましょう。
まとめ
key は、リストの各要素が「どれなのか」を React に伝えるための目印です。React はこの key を頼りに、要素の追加・削除・並べ替えを効率よく見分け、状態を正しい要素に結びつけます。配列の index を key にすると、並び替えや先頭への追加で位置番号がずれ、入力欄の値やチェック状態が別の要素に移ってしまう不具合が起きます。key には、データが持つ一意で安定した ID を使うのが基本です。並び順が固定で状態も持たないリストなら index でも問題ありませんが、迷ったら ID を使う——これを覚えておけば、リスト表示で困ることはほとんどなくなります。