Menu

Reactのリストとkey:map()、key prop、indexをkeyにする問題

Reactでリストを描画するには、配列に対してmap()を呼び、各項目のJSXを返して、すべての項目にkey propを付けます。filterしてからmapする方法、keyの値をどこから取るか、そしてリストが変わると配列のindexのkeyが項目を取り違える理由を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

Reactでリストを描画するには、配列に対して map() を呼び、各項目に対してJSXを返します。各項目の一番外側の要素には、その項目を識別する値(通常はID)を持つ key propを付けてください。そうすればリストが変わったときに、Reactが項目を区別できます。

配列に { id: 'dat', name: 'Date', price: 4 } を追加すると、4行目が現れます。JSXには独自のループ構文はありません。map が要素の配列を返し、Reactは配列を各要素を順番に描画することで描画します。

mapで配列を描画する

map は各項目ごとに関数を1回呼び、結果を新しい配列に集めます。JSXの波かっこの中ではその配列はただの値なので、上の例のようにマークアップの中で直接mapすることも、先に配列を作っておくこともできます。

const items = fruits.map((fruit) => <li key={fruit.id}>{fruit.name}</li>);
return <ul>{items}</ul>;

つまずきやすい点が2つあります。アロー関数で波かっこを使うなら return が必要です:fruits.map((f) => { return <li key={f.id}>{f.name}</li>; })。これがないと関数は undefined を返し、リストは空になります。また、key は map から返す要素に付け、その中の要素には付けません。項目がコンポーネントなら、コンポーネントにkeyを付けます:<FruitRow key={fruit.id} fruit={fruit} />。

filterしてからmapする

項目の一部を表示するには、先に配列をフィルターして、その結果をmapします。各メソッドが1つの仕事をし、メソッドチェーンは「条件に合う果物を、リスト項目として」という文のように読めます。

検索欄に er と入力すると、リストはEraser、Ruler、Staplerに絞られます。In stock onlyにチェックを入れるとRulerも消えます。フィルターしたリストは2つのstateからレンダリング中に計算されるので、同期を保つべき2つ目の配列をstateに持つ必要はありません。visible.length === 0 && は比較が本物の真偽値なので、&& の安全な使い方です(ただの 0 だとどうなるかは条件付きレンダリングのページで紹介しています)。

keyの役割

リストが再レンダリングされると、Reactは新しい要素の配列を前回のものと比べます。keyはその対応付けの手段です。今のkey 3 の要素は、たとえ移動していても、前のkey 3 の要素と同じものです。Reactはその項目のDOMノードとコンポーネントのstateを保ち、変わったものだけを移動したり更新したりします。新しいkeyの項目は作られ、消えたkeyの項目は取り除かれます。

keyがなければ、Reactが対応付けに使えるのは位置だけです。何も動かない間はそれで問題ありませんが、何かが動いた途端に間違いになります。

indexのkeyが壊れる理由

map は第2引数としてindexを渡すので、key={index} と書きたくなります。しかしindexが表すのは位置であって、項目ではありません。先頭に項目を挿入するとすべての項目のindexが1つずれるので、Reactは項目0はまだ項目0だと考え、古いstateをそこに残します。

次のデモは同じリストを2回、indexをkeyにしたものとIDをkeyにしたもので描画します。各行には自分のテキストを保つ入力欄があります。

両方のリストでAdaの横に math と入力してから、Add to topをクリックしてください。IDのリストではメモはAdaに残ります。indexのリストではメモが新しい人に移ります。最初の入力欄は位置0の入力欄のままで、位置0は今では別の人だからです。名前はデータから来るのでどちらのリストでも正しく、おかしくなるのはDOMやコンポーネントの中にあるstateだけです。実際のアプリでは、チェックボックス、フォーカス、アニメーション、そして useState を持つあらゆるコンポーネントでこの問題が起きます。

indexのkeyが安全なのは、次のすべてが成り立つときだけです。リストが並べ替えもフィルターもされない、末尾以外で項目が挿入も削除もされない、項目が自分のstateを持たない。フッターのリンクの固定リストなら該当します。ユーザーが編集できるものは、たいてい該当しません。

keyをどこから取るか

一番よいkeyは、データにすでにあるIDです。データベースのID、商品のSKU、スラッグ、ユーザー名などです。サーバーからのデータにはほぼ必ずあります。

ブラウザで作る項目なら、作るときに各項目にIDを付けて項目と一緒に保存します。上のデモのようなカウンターか、crypto.randomUUID() です。keyをレンダリング中に生成してはいけません。

// Wrong: a new key on every render, so React remounts every item every time
{todos.map((todo) => <Todo key={Math.random()} todo={todo} />)}

// Right: the ID is created once, in the event that adds the item
function addTodo(text) {
    setTodos([...todos, { id: crypto.randomUUID(), text }]);
}

レンダリングのたびに変わるkeyはindexより悪い結果になります。すべての項目が毎回破棄されて作り直され、入力中にフォーカスが外れ、何もstateを保てません。この仕組みは意図的に使うこともできます。コンポーネントのkeyを変えるとリセットされるので、useStateのページではそれを使ってフォームをクリアしています。

keyが一意であるべきなのは兄弟の間だけ

keyは1つのリストの中で一意であればよく、アプリ全体で一意である必要はありません。Reactは同じ親の子どうしでしかkeyを比べないので、別々のリストは同じIDを再利用できます。

どちらのセクションにもkeyが 1 と 2 の料理がありますが、これは正しいです。それぞれの ul が独立したリストだからです。問題になるのは1つのリストの中でkeyが重複することです。Reactは衝突した項目を区別できず、リストが更新されたときに項目を重複させたり落としたりすることがあります。

1つの項目が <dt> と <dd> のように複数の兄弟要素を描画するときは、<Fragment key={item.id}> で囲みます。短い <> 構文はkeyを受け取れません。

「unique key prop」の警告

keyを省くと、開発中にReactはブラウザのコンソールに次のように出力します。

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

Check the render method of `App`.

2行目は、そのリストを生み出したレンダリングを持つコンポーネントの名前で、どの map を直せばよいかがわかります。同じkeyを持つ2つの兄弟には別のメッセージが出て、「Encountered two children with the same key」で始まります。このページのプレビューは本番ビルドのように動き、そのチェックを省くので、key を削除してもここでは警告は表示されません。それでも最初の例で1つ削除してみてください。keyがないことは正しさの問題であってクラッシュではないので、リストは描画されたままです。警告を直すには、map から返す要素に安定したkeyを付けます。key={index} を付けると警告は消えますが、上で見たバグが戻ってきます。

また、key は自分のコンポーネントには渡されないことに注意してください。FruitRow の中では props.key は undefined です。コンポーネントがIDを必要とするなら、普通のpropとしてもう一度渡してください:<FruitRow key={f.id} id={f.id} />。

よくある質問

Reactのリストの項目にkeyが必要なのはなぜですか?

keyは、レンダリング間でどの項目がどれなのかをReactに伝えます。配列が変わると、Reactは古い項目と新しい項目をkeyで対応させるので、位置で推測するのではなく、各項目のstateとDOMノードを正しいデータとともに保てます。

配列のindexをkeyとして使えますか?

順番が変わらず、途中で項目が挿入も削除もされないリストに限ります。項目が動く可能性があるなら、indexのkeyはstateを位置に結びつけるので、入力したテキスト、チェックボックス、フォーカスが別の項目に移ってしまいます。

keyには何を使えばよいですか?

データに属するIDです。データベースのID、商品のSKU、ユーザー名などです。ブラウザで作られる項目なら、項目を作るときに1回だけIDを生成してください(カウンターや crypto.randomUUID())。レンダリング中には決して生成しないでください。

keyはアプリ全体で一意である必要がありますか?

いいえ。keyは同じリスト内の兄弟の間で一意であればよく、別々のリストなら同じkeyを使えます。

コンポーネントは自分のkeyを読めますか?

いいえ。key はReactが使うもので、propとしてコンポーネントには渡されません。コンポーネントがIDを必要とするなら、id={item.id} のように別の名前でもう一度渡してください。

"Each child in a list should have a unique key prop" はどう直しますか?

map から返す一番外側の要素に、項目の安定したIDを使って key を付けます。その要素がフラグメントなら、<> はkeyを受け取れないので <Fragment key={id}> を使ってください。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める