この記事の目次
一覧を逆順にしたら、入力メモが違う資料の横へ移ってしまった。Reactのkeyは警告を消すための番号ではなく、次の描画でも同じ項目を識別する手掛かりです。並び順を変えても入力を保つには、位置ではなくデータに付いたIDを使います。
JSX、props、useState、配列のmapが分かる人向けです。架空の機材点検一覧を作り、各行に一時的なメモを入力します。サーバー保存は行いません。ここでは「行の順序が変わっても、その機材のメモであり続ける」ことを確認します。
先頭の行と、三脚の行は同じ意味ではない
最初の並びが三脚、ライト、マイクなら、三脚の位置は0です。逆順にすると位置0はマイクになります。keyに配列のindexを使うと、同じ位置の行を続けて使う判断になり、その行が持っていたstateと新しい機材名が食い違うことがあります。
keyをtripodのような変わらないIDにすると、並べ替え後も三脚の行を対応させられます。React公式のリスト描画の説明では、兄弟間で一意であること、描画のたびに作り直さないことが示されています。ページ全体で一つしか使えないIDという意味ではありません。
| 識別に使う値 | 逆順にした後 | 今回の採用判断 |
|---|---|---|
| 配列内の位置0 | 別の機材を指す | 不採用 |
| 機材名 | 名称変更や同名で変わる・重なる | 不採用 |
| データの固定ID | 同じ機材を指す | 採用 |
配列をコピーして並べ替える記事は、元の順序を壊さないための操作です。配列を正しくコピーしていても、Reactが行を識別するkeyが位置のままなら、今回の入力の取り違えは残ります。
行ごとのメモを持つ、小さな点検一覧
Node.js 24系とnpmを使い、空の練習フォルダで次を実行します。比較できるようReact / React DOM 19.2.0、Vite 7.1.7に固定します。Next.jsや追加のルーティング機能は使いません。
npm init -y
npm install --save-exact react@19.2.0 react-dom@19.2.0
npm install --save-dev --save-exact vite@7.1.7
次をindex.htmlへ保存します。短い画面ですが、名前とメモ欄の対応、キーボードの現在位置も確認できるようにします。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>機材の点検メモ|練習</title>
<style>
* { box-sizing: border-box; }
body { margin: 16px; font-family: sans-serif; line-height: 1.8; }
main { max-width: 560px; margin: auto; }
ul { list-style: none; padding: 0; }
li { border: 1px solid #777; padding: 16px; margin-block: 12px; }
input { display: block; width: 100%; font: inherit; }
button { font: inherit; margin: 4px; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>
続けてmain.jsxを同じ場所へ作ります。RowはAppの外で定義し、行の種類まで毎回変わる構成を避けています。
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
const initialItems = [
{ id: 'tripod', name: '三脚' },
{ id: 'light', name: 'ライト' },
{ id: 'mic', name: 'マイク' }
];
function Row({ item }) {
const [note, setNote] = useState('');
return <li>
<label htmlFor={`note-${item.id}`}>{item.name}の点検メモ</label>
<input id={`note-${item.id}`} value={note}
onChange={event => setNote(event.target.value)} />
</li>;
}
function App() {
const [items, setItems] = useState(initialItems);
return <main>
<h1>機材の点検(架空の練習)</h1>
<p>メモは保存・送信しません。再読み込みすると初期状態へ戻ります。</p>
<button type="button" onClick={() => setItems(list => [...list].reverse())}>
逆順にする
</button>
<button type="button" disabled={items.length === 0}
onClick={() => setItems(list => list.slice(1))}>
先頭の機材を外す
</button>
<ul>{items.map(item => <Row key={item.id} item={item} />)}</ul>
{items.length === 0 && <p>表示する機材はありません。</p>}
</main>;
}
createRoot(document.getElementById('root')).render(<App />);
npx vite --host 127.0.0.1で起動し、表示されたURLを開きます。Viteの公式ガイドで説明されるHTMLを入口にした構成です。三脚の欄へ「脚の固定を確認」と入力し、逆順にすると、三脚は末尾へ移り、同じメモがその横に残ります。
keyを付ける場所は、mapが返す要素
mapが直接返すのはRowなので、keyもRowに指定します。Rowの内部のliだけへ付けても、親が並べるRow同士を区別するためのkeyにはなりません。HTMLへ付けるid属性とも役割が違います。
keyはRowに通常のpropsとして渡りません。今回のRowが機材IDを使えるのは、itemを別途渡しているためです。inputのidはlabelから参照する名前、RowのkeyはReactが行を識別する名前として使い分けます。
メモは各Rowにあります。共通の親へstateを移す記事と違い、今回は別の行と共有しない一時メモです。共有する必要がないことと、行の対応を安定させる必要があることは両立します。
indexで取り違える失敗と、毎回の採番で消える失敗
第一の比較では、mapの一行だけを次へ置き換えます。これは不具合を再現するためのコードです。ページを再読み込みし、三脚だけにメモを入力してから逆順にします。
{items.map((item, index) => <Row key={index} item={item} />)}
先頭のメモがマイクの横へ残れば、位置に状態が対応している症状です。修正は完成例のitem.idへ戻すことです。変更後は再読み込みしてから同じ操作をし、三脚のメモが三脚と一緒に末尾へ動くか確認します。修正前に取り違えた値を自動で復元する機能ではありません。
第二の失敗は、keyへMath.random()を使うことです。親が描画し直されるたび別のkeyになるため、逆順ボタンを押すと行が新しく作られ、メモが空へ戻る原因になります。毎回重複しなければよいのではなく、同じ項目には同じ値を使い続けます。
新規登録時にIDを一度作る方法は使えますが、mapの中で毎回作る方法とは異なります。今回の修正はデータ内の固定IDへ戻すことです。二回逆順にしても、各機材のメモが失われず元の順序へ戻るかを再確認します。
行を外す操作は、保存とは別に考える
安定したkeyがあっても、一覧から消えたRowのローカルstateを永続保存できるわけではありません。React公式の状態保持の説明では、ツリーから取り除いたコンポーネントのstateが失われることを確認できます。
絞り込みで一度隠した行にも入力を残したいなら、親にID別のメモを持つなど、Rowの外へ保存先を移す設計が必要です。実際に送信する点検結果なら、サーバーへの保存も別途考えます。keyは保存処理の代わりにはなりません。
変化しない説明の羅列では、入力の取り違えが目立たない場合もあります。それでも今後並べ替えや削除を追加する一覧なら、データが持つIDを使える形にしておくと、今回の条件をそのまま検査できます。名前の重複を勝手に禁止してkeyの都合へ合わせる設計にはしません。
並べ替え、削除、再読み込みの結果を分ける
開発サーバーを止めてnpx vite build、続けてnpx vite preview --host 127.0.0.1を実行します。15〜20分を目安に公開用ビルドで次を確認してください。
- 三脚とライトに別々のメモを書き、逆順にしても名前とメモの組が変わらない。
- 逆順の先頭であるマイクを外しても、ライトと三脚のメモはそのまま残る。
- さらに全行を外すと空の案内が出て、削除ボタンが無効になる。
- ページを再読み込みすると三件に戻り、メモは空になる。
Tabで各欄とボタンへ進めること、320px幅で入力欄がはみ出さないことも確認します。カリキュラムの学習項目と照合するときは、成果を「警告を消せた」ではなく、「項目のIDと入力状態の対応を操作で確かめられた」として記録できます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。