React / Next.js公開 約10分

Reactのkeyで並べ替え後も入力を保つ:位置と項目のIDを分ける

一覧を逆順にすると入力メモが別の項目へ移る場合に。固定IDをkeyにしたReactの点検一覧を作り、indexや毎回変わる値との違いを比較します。行を外した後の状態保持の限界も確かめます。

この記事の目次
  1. 先頭の行と、三脚の行は同じ意味ではない
  2. 行ごとのメモを持つ、小さな点検一覧
  3. keyを付ける場所は、mapが返す要素
  4. indexで取り違える失敗と、毎回の採番で消える失敗
  5. 行を外す操作は、保存とは別に考える
  6. 並べ替え、削除、再読み込みの結果を分ける

一覧を逆順にしたら、入力メモが違う資料の横へ移ってしまった。Reactのkeyは警告を消すための番号ではなく、次の描画でも同じ項目を識別する手掛かりです。並び順を変えても入力を保つには、位置ではなくデータに付いたIDを使います。

JSX、props、useState、配列のmapが分かる人向けです。架空の機材点検一覧を作り、各行に一時的なメモを入力します。サーバー保存は行いません。ここでは「行の順序が変わっても、その機材のメモであり続ける」ことを確認します。

先頭の行と、三脚の行は同じ意味ではない

最初の並びが三脚、ライト、マイクなら、三脚の位置は0です。逆順にすると位置0はマイクになります。keyに配列のindexを使うと、同じ位置の行を続けて使う判断になり、その行が持っていたstateと新しい機材名が食い違うことがあります。

keyをtripodのような変わらないIDにすると、並べ替え後も三脚の行を対応させられます。React公式のリスト描画の説明では、兄弟間で一意であること、描画のたびに作り直さないことが示されています。ページ全体で一つしか使えないIDという意味ではありません。

識別に使う値 逆順にした後 今回の採用判断
配列内の位置0 別の機材を指す 不採用
機材名 名称変更や同名で変わる・重なる 不採用
データの固定ID 同じ機材を指す 採用

配列をコピーして並べ替える記事は、元の順序を壊さないための操作です。配列を正しくコピーしていても、Reactが行を識別するkeyが位置のままなら、今回の入力の取り違えは残ります。

逆順にしたとき、位置のkeyではメモがマイクへ移り、固定IDでは三脚に残る比較図

行ごとのメモを持つ、小さな点検一覧

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

記事の内容を、課題で確かめる

前提になるJavaScriptの練習 →

React・TypeScriptの学習内容 →

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る