3.7 リストと key — なぜ必要か
この章で掴んでほしいのは、「key は 前回レンダーと今回レンダーの間で、どの要素が同じものか を React に教えるための識別子である」という一点です。配列の長さや順序が変わる可能性がある以上、React はこの情報なしには差分を正しく計算できません。
最初は「key なんて単なる warning を消すためのおまじない」くらいに見えます。でもここをしっかり理解すると、後で遭遇する「入力欄が別の行に紐づいて見える」系のバグが一気に分かるようになります。
リストの作り方
React で動的なリストをレンダーするときは、配列の map() で要素の配列に変換します。
type Todo = { id: string; text: string }
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}公式はこう念を押しています。
JSX elements directly inside a
map()call always need keys!
key を付けないと、React は開発時に警告を出します。
key は何のためにあるか
React は Virtual DOM の差分計算で「前回と今回で、同じ要素 / 違う要素を区別する」必要があります。key はこの対応付けを支える情報です。
Keys tell React which array item each component corresponds to, so that it can match them up later. This becomes important if your array items can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key helps React infer what exactly has happened, and make the correct updates to the DOM tree.
出典: 同上
公式はファイルシステムの比喩を使っています。
Imagine that files on your desktop didn't have names. Instead, you'd refer to them by their order -- the first file, the second file, and so on.
出典: 同上
ファイルに名前があれば、順番が入れ替わっても「これが取扱説明書、これが請求書」と追跡できます。名前 (= key) がなければ、「1 番目」「2 番目」という位置でしか追えません。
図 3.7.1 key がない場合と ある場合の比較
key が位置 (index) だけの場合、React は「[0] が A → X に、[1] が B → A に、[2] が C → B に変わり、[3] に C が追加された」と解釈します。実際には「X だけが先頭に挿入された」だけなのに、DOM 全体を書き換える計算になってしまいます。
一方、各要素に安定した id (A, B, C, X) を key として与えていれば、React は「X が先頭に挿入されただけ」と正しく判断できます。
図 3.7.2 安定した key で「中身が引っ越す」イメージ
安定した key を使うと、位置が変わっても React は「この行の中身 (state や入力値) は元のまま連れて行ってあげよう」と動きます。文字でも分かりますが、並べて見るとイメージしやすいです。
key のルール
公式が挙げる守るべきルールは 2 つです。
- 兄弟の中で一意 — 同じ配列内で重複してはいけない (別の配列なら同じ値でも可)。
- レンダーをまたいで同じ — 生成し直してはいけない
実務で使える例:
- DB のプライマリキー (
todo.id) - サーバー発行の UUID
crypto.randomUUID()でデータ生成時に割り振る (生成時 であって、レンダー時ではない)
なぜ index を key にすべきでないか
初学者が真っ先に試すのが key={index} です。公式は強く戒めています。
You might be tempted to use an item's index in the array as its key. In fact, that's what React will use if you don't specify a
keyat all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs.出典: 同上
index を key にすると、並べ替え・挿入・削除のたびに「どの要素がどれか」の対応付けが崩れます。表示上は正しく見えても、子コンポーネント内の state (入力中の文字など) が別の要素に紐づいたように見えるバグになります。
具体例 — 入力中のテキストが別のアイテムに移動する
type Item = { id: string; label: string }
function Row({ label }: { label: string }) {
const [note, setNote] = useState('')
return (
<li>
<span>{label}</span>
<input value={note} onChange={(e) => setNote(e.target.value)} />
</li>
)
}
export default function App() {
const [items, setItems] = useState<Item[]>([
{ id: 'a', label: 'りんご' },
{ id: 'b', label: 'バナナ' },
{ id: 'c', label: 'ぶどう' },
])
return (
<>
<ul>
{items.map((item, i) => (
// NG: 並べ替えるとメモの内容が別の行に残ってしまう
<Row key={i} label={item.label} />
))}
</ul>
<button onClick={() => setItems([...items].reverse())}>反転</button>
</>
)
}「バナナ」の行のメモ欄に「美味しい」と入れて反転ボタンを押すと、key={i} では [0], [1], [2] という位置だけを見るので、[1] (並べ替え後は「バナナ」以外) の入力欄に「美味しい」が残ります。
修正は簡単で、安定した id を key に使う だけです。
<Row key={item.id} label={item.label} />React は「id=a の行はここに移動した」「id=b の行はあそこに移動した」と正しく追跡し、入力中の state も一緒に付いていきます。
触って違いを体感してみる
このバグは一度自分の目で見ると印象に残ります。下のデモは key={i}(index)と key={item.id}(id)を並べて置いていて、両方に同じ並べ替えをかけられます。いくつかの行にメモを書き込んで、「並べ替え」を押してみてください。
4 つの項目の各行にメモ欄があります。いくつかに文字を入れたあと 「並べ替え」 ボタンを押して、メモが正しい項目に付いていくか比べてみてください。
index を key に使う- りんご
- バナナ
- ぶどう
- メロン
入力して並べ替えると、メモが「位置」に残って項目とずれます。
安定した id を key に使う- りんご
- バナナ
- ぶどう
- メロン
並べ替えてもメモは項目とセットでついていきます。
左側は「文字が位置に取り残される」、右側は「文字が項目と一緒に移動する」という違いがはっきり出るはずです。
key と state の保持 — 位置と識別の関係
key の話は、より深い「state は位置 (+ key) で識別される」という仕組みにつながります。
React preserves a component's state for as long as you render the same component at the same position in the tree.
つまり React から見ると、コンポーネントの同一性は「ツリーのどの位置にあるか」で決まります。リストの中では位置だけでは足りないので、key が補助します。
この仕組みを使い、「key を明示的に変えることで state を強制的にリセットする」というテクニックもあります。
// userId が変わるたびに Profile を作り直し、内部 state を空にする
<Profile userId={userId} key={userId} />これは 3.6 useEffect で触れた「Effect を使わず state をリセットする方法」そのものです。
key は props ではない
注意点として、key は React のための 予約語 であり、子コンポーネントの props として渡りません。
Your components won't receive
keyas a prop. It's only used as a hint by React itself. If your component needs an ID, you have to pass it as a separate prop:<Profile key={id} userId={id} />.
コンポーネント内で id を参照したい場合は、別の props (例: userId) を明示的に渡す必要があります。
まとめ
- リストレンダー (
map) には必ずkeyを付ける。 keyは「前回と今回で同じ要素か」を React に教える識別子。- index を key にすると、並べ替え・挿入・削除で state が混ざるバグが出る。
- 安定した id (DB id、UUID) を使う。
keyを意図的に変えると、コンポーネントを作り直して state をリセットできる。
さらに深く学ぶには
- react.dev — Rendering Lists — 完全な説明と練習問題。
- react.dev — Preserving and Resetting State — 位置と key が state に与える影響。
- react.dev — Reconciliation (legacy doc) — 内部の差分計算アルゴリズムの解説 (Part 6 で深掘り)。