リストのkeyにindexを使うと何が起きるか
リストのkeyにindexを使うと何が起きるか
Reactで配列を描画すると、コンソールに Each child in a list should have a unique "key" prop. という警告が出ます。これをとりあえず黙らせようと key={index} を渡して、あとで地味なバグに悩まされたので、そのときのメモです。
なぜ key が必要で、なぜindexだとまずいのかを順に整理します。
keyはReactにとっての「同一性の目印」
Reactは再描画のたびに、前回のリストと今回のリストを突き合わせて「どの要素が増えた・減った・移動した」かを判断します。このときの照合に使うのが key です。key が同じなら「同じ要素が続いている」とみなして、対応するDOMやコンポーネントのstateを再利用します。
つまり key は「リスト内のどの項目か」を一意に指す 安定した識別子 である必要があります。データ自体が持つIDがあれば、それが最適です。
{users.map((user) => (
<UserRow key={user.id} user={user} />
))}
indexがまずいのは「順番が変わる」とき
問題は、key に配列のindexを使った場合です。indexは「今その要素が何番目にあるか」でしかなくて、項目そのものには結びついていません。並べ替えや先頭への挿入が起きると、同じ項目でもindexが変わってしまいます。
具体的な壊れ方を見てみます。各行が入力欄を持つフォームで、先頭に行を追加するケースです。
function TodoList() {
const [todos, setTodos] = useState(['牛乳を買う', '本を返す'])
const addToTop = () => setTodos(['(新規)', ...todos])
return (
<div>
<button onClick={addToTop}>先頭に追加</button>
{todos.map((todo, index) => (
// 危険:keyがindex
<input key={index} defaultValue={todo} />
))}
</div>
)
}
ここで各 input に何か入力してから「先頭に追加」を押すと、入力した文字が 一つ下の行にずれて残る という挙動になります。最初これを見たとき、何が起きたのか分からず固まりました。
Reactから見ると key=0 の要素はずっと存在し続けている(中身の defaultValue が変わっただけ)と判断されて、input のDOM(=ユーザーが打ち込んだ値)はそのまま再利用されるためです。
defaultValue や、コンポーネント内部の useState、フォーカス状態など「DOM側・React側が保持しているもの」はすべてこのズレの影響を受けます。
安定したkeyを用意する
解決策は、indexではなく 項目に固有で変化しない値 をkeyにすることです。
type Todo = { id: string; text: string }
const [todos, setTodos] = useState<Todo[]>([
{ id: 'a1', text: '牛乳を買う' },
{ id: 'b2', text: '本を返す' },
])
{todos.map((todo) => (
<input key={todo.id} defaultValue={todo.text} />
))}
サーバー由来のデータならたいていIDがあります。クライアントだけで作る項目には、生成時に crypto.randomUUID() などでIDを振っておくと安心です。
indexをkeyにしてもよい条件
とはいえ、常にindexが悪いわけではないようです。次の3つを すべて 満たすなら、indexでも実害は出ません。
| 条件 | 内容 |
|---|---|
| 並べ替えがない | 要素の順序が固定されている |
| 末尾以外の追加削除がない | 途中への挿入や削除をしない |
| 項目がstateを持たない | 各項目が入力欄や内部stateを抱えていない |
静的な表示専用リストなどはこれに当てはまります。逆に、並べ替え・途中挿入・入力欄のどれか一つでもあるなら、固有IDを使うべきだと考えておくとよさそうです。
まとめ
keyはリスト要素の同一性をReactに伝える目印で、安定して一意であることが前提- indexをkeyにすると、並べ替えや先頭挿入でstateやDOMが取り違えられる
- 原則は項目固有のIDを使う。完全に静的なリストに限ってindexも許容できる
判断に迷ったら「この項目の順序や数は将来変わりうるか?」を問うのが近道だと思います。詳しい挙動はReact公式のRendering Listsにも整理されています。以上です。