質問と適用シナリオ
管理ページで編集可能なユーザーリストをレンダリングしています。各 Row は未送信の入力下書きと展開状態を保持しています。ユーザーは先頭に レコードを挿入したり、中間のレコードを削除したり、並べ替えやフィルタリングを行ったりできます。リストの横にある詳細フォームは、userId が 変更されるたびに前のユーザーのローカル状態をクリアする必要があります。
以下を説明してください:
- React が隣接するレンダリング間でコンポーネントが同一であるかをどのように判断するか。
- なぜ
key={index}が下書きや展開状態を別のレコードに移動させてしまう可能性があるのか。 - なぜ
key={Math.random()}がコンポーネントを繰り返し再マウントするのか。 - 安定したドメイン ID で状態を保持すべきなのはいつか、また key を変更してサブツリーをリセットすべきなのはいつか。
- 挿入、削除、並べ替え、フィルタリング、エンティティの切り替え全体で動作を検証する方法。
2026年3月および5月に公開された React 面接資料には、key、インデックス key、調整(reconciliation)、再マウントが直接含まれています。 この設問はリストの構文のように見えますが、候補者がどの論理エンティティが状態を所有しているかをモデル化し、そのモデルをコンポーネントの境界とテストを通じて表現できるかをテストしています。
その核心的な能力が React コンポーネントの同一性、状態のライフタイム、DOM の再利用にあるため、これはフロントエンドの質問です。
面接官が評価していること
第一に、状態が JSX タグやドメインオブジェクトに自動的に紐づくわけではないことを候補者が理解している必要があります。React は状態を レンダーツリー内の位置に関連付けます。単一の親の中では、要素のタイプと key が、古い子と新しい子が同一の識別性を表しているかを判断するのに役立ちます。
第二に、候補者は再レンダリングと再マウントを区別できなければなりません。新しい props や親の更新によって、ローカル状態を保持したまま 同じ識別性を再レンダリングできます。コンポーネントタイプや key の変更は新しい識別性を作成し、古いサブツリーのローカル状態を破棄し、 その DOM を再作成する可能性があります。
第三に、候補者はドメインの識別性から key を導出すべきです。データベース ID や、レコード作成時に生成・保存された UUID は、通常 「これは依然として同じレコードである」ことを意味します。配列の位置、現在のタイムスタンプ、またはレンダリング中に生成されたランダムな値はそうではありません。
第四に、優れた回答は「key を決して変更してはならない」というルールに固執しません。詳細フォームがユーザー A からユーザー B に切り替わるとき、 それらのフォームは異なるドメインエンティティを表している可能性があります。正しい境界での key={userId} は、Effect で個々の状態変数をクリアするよりも 確実にフォームサブツリー全体をリセットできます。
最後に、候補者は再マウントのコストを挙げる必要があります。フォーカス、スクロール位置、未送信の入力、子孫の状態が すべて失われる可能性があります。エンティティが再び選択されたときに製品が下書きを復元する必要がある場合は、削除されたサブツリー内のローカル状態に依存するのではなく、 下書きをリフトアップし、エンティティごとに保存するか、外部に永続化してください。
回答前の明確化のための質問
- リストはアイテムの挿入、削除、並べ替え、またはフィルタリングが可能ですか? メンバーシップや順序が変更される可能性がある場合、配列インデックスは
ドメインエンティティを一意かつ安定して識別しません。
- 各行は React またはブラウザの DOM 状態を保持していますか? 入力、展開状態、アニメーション、非制御フィールドは、不適切な
再利用をすぐに露呈させます。
- データには兄弟間で一意の安定した ID がありますか? key にはグローバルな一意性ではなく、兄弟間の一意性が必要です。
- エンティティの切り替え時に下書きを破棄すべきですか、それとも復元すべきですか? 破棄する場合は key の変更が適しています。復元する場合は、より長寿命な
状態レイヤーが必要です。
- サブツリー全体をリセットすべきですか、それとも1つのフィールドのみをリセットすべきですか? 完全な識別性のリセットには key を使用し、
部分的な調整には制御された状態(controlled state)や明示的なデータ更新を優先してください。
- ID はいつ生成されますか? ローカルレコードは作成時に UUID を受け取り、それを保持することができます。レンダリングごとに
新しい UUID を生成しないでください。
30秒の回答フレームワーク
「React は状態をレンダーツリー内の位置に関連付けます。同じ親の下では、コンポーネントタイプと key が、古いノードと新しいノードが同一の識別性を持つかどうかを React が判断するのに役立ちます。安定した key を使用すると、行の状態が移動してもドメインレコードに追従できます。配列インデックスを使用すると、挿入や並べ替えによって同じスロットに別のレコードが配置される可能性があり、ローカルの下書きが誤った行に移動してしまうことがあります。ランダムな key は前回のレンダリングと決して一致しないため、React はコンポーネントと DOM を繰り返し再作成します。
リストにはデータからの安定した ID を使用すべきです。ユーザー A の詳細フォームからユーザー B への切り替えですべてのローカル状態をクリアする必要がある場合は、フォームサブツリーの境界に key={userId} を配置して、これが新しいエンティティであることを示します。各ユーザーの下書きを保持する必要がある場合は、識別性の境界を維持しながら下書きをリフトアップして userId ごとに保存します。挿入、削除、並べ替え、フィルタリング、同一 ID での再レンダリング、異なる ID への切り替えを検証し、状態が意図したエンティティに追従することを証明します。」
ステップバイステップの詳細解説
ステップ 1: コンポーネントの識別性モデルを構築する。
面接で役立つモデルは以下のとおりです:
| 古いレンダリングと新しいレンダリングの関係 | 典型的な結果 |
|---|---|
| 同じ親、同じコンポーネントタイプ、同じ key | その識別性のローカル状態を保持。コンポーネントは再レンダリングされる可能性がある |
| 同じ親、同じタイプ、異なる key | 古い識別性を削除し、新しい識別性をマウントして、サブツリーの状態をリセットする |
| 同じ位置、異なるコンポーネントタイプ | 古いサブツリーを置き換え、状態をリセットする |
| 明示的な key のないリスト | 位置ベースのマッチングにフォールバックする(動的リストでは安全ではない) |
key はコンポーネントに渡される通常の prop ではありません。これは React へのヒントです。Row がドメイン ID も必要とする場合は、 rowId={item.id} などの別の prop を渡してください。
また、key は現在の親の中でのみ識別性を定義します。2つの別々のリストが両方とも key="user-42" を含むことは問題ありませんが、 1つのリスト内の兄弟要素間で key が重複してはなりません。
ステップ 2: 編集可能リストでインデックス key のバグを再現する。
誤った実装:
function UserList({ users }: { users: User[] }) {
return users.map((user, index) => (
<EditableRow key={index} user={user} />
))
}初期順序が [Alice, Bob] であるとします。インデックス 0 の EditableRow には Alice の下書きが保存されています。先頭に Zoe を挿入すると、 [Zoe, Alice, Bob] になります。React は key 0 を持つ古い行をインデックス 0 の新しいアイテムにマッチさせる可能性があるため、Alice に属していた状態が Zoe の行に表示されることがあります。key 1 の状態も同様に Bob から Alice に移動する可能性があります。
問題はインデックスが数値であることではありません。インデックスはスロットを識別しますが、プロダクトはユーザーを識別する必要があります。削除、 並べ替え、フィルタリングはすべて、スロットとエンティティのマッピングを変更します。
正しい実装:
function UserList({ users }: { users: User[] }) {
return users.map((user) => (
<EditableRow key={user.id} rowId={user.id} user={user} />
))
}Zoe が挿入された後も、Alice は Alice の ID を保持し続けます。彼女はインデックス 0 からインデックス 1 に移動できますが、React は引き続き Alice のコンポーネント状態を Alice にマッチさせることができます。
1つのレコードが複数の兄弟ノードを返す場合、短い Fragment 構文(<>)は key を取ることができません。明示的な Fragment を使用してください:
import { Fragment } from 'react'
users.map((user) => (
<Fragment key={user.id}>
<UserHeading user={user} />
<EditableRow user={user} />
</Fragment>
))ステップ 3: レンダリング中に生成するのではなく、安定した key を選択する。
実用的な優先順位は次のとおりです:
- バックエンドまたはデータベースからの安定したレコード ID。
- データにすでに付与されており、そのドメインライフタイム全体で変更されない一意の識別子。
- ローカル専用の新しいレコードの場合、レコード作成イベントで生成され、レコードとともに永続化された UUID。
- そのフィールドが真に不変で兄弟間で一意のドメイン識別性を形成する場合にのみ使用する複合 key。
以下は、レンダリングごとに新しい識別性を作成してしまいます:
<EditableRow key={Math.random()} user={user} />これは通常の再レンダリングではありません。React は古い行を削除し、新しい行をマウントします。ローカル状態とユーザー入力は失われ、 DOM が再作成されます。Date.now() やレンダリング中の crypto.randomUUID() の呼び出しも同じ問題を引き起こします。UUID は、アイテムのレンダリング中に生成するのではなく、 アイテム作成時に一度生成して保存するのであれば問題ありません。
配列インデックスが許容されるのは、極めて限定的な条件の下のみです。リストの全ライフタイムにわたってメンバーシップと順序が固定され、 挿入、削除、フィルタリング、並べ替えがなく、位置そのものが識別性であり、ドメインエンティティに追従すべき状態が存在しない場合です。 動的なビジネスデータは、これらの前提に依存するのではなく、実際の ID を受け取るべきです。
ステップ 4: 「これは別のフォームである」ことを表現するために key を使用する。
詳細ページでよく試みられる修正は次のとおりです:
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState('')
useEffect(() => {
setComment('')
}, [userId])
return <CommentForm value={comment} onChange={setComment} />
}これはまず古いコメントでツリーをレンダリングし、Effect が実行された後にもう一度レンダリングをトリガーします。さらに重要なことに、 深い詳細ページには添付ファイル、バリデーションエラー、ネストされたフォーム状態も含まれる場合があります。1つの comment 変数をクリアしても、 サブツリー全体がリセットされる保証はありません。
製品が各ユーザーのフォームを個別のエンティティとして定義している場合は、識別性の境界に key を配置します:
function ProfilePage({ userId }: { userId: string }) {
return <ProfileForm key={userId} userId={userId} />
}userId が変更されると、React は新しい ProfileForm を異なる識別性として扱い、そのローカル状態とすべての子孫の状態を リセットします。無関係な親の状態によって同じ userId で再レンダリングが発生した場合、key は安定したままであり、ローカル状態は 保持されます。
リセットが必要な最小限の完全な境界に key を配置してください。ページ全体に key を設定すると、ナビゲーション、コストの高い表示、 無関係なスクロール状態も再作成されてしまいます。1つの input だけに key を設定すると、同じフォームの他の状態が残ってしまう可能性があります。
ステップ 5: 再マウントを選択する前に、製品が何を保持すべきかを決定する。
「エンティティの切り替え」は自動的に「下書きの破棄」を意味するわけではありません。決定テーブルを使用してください:
| 製品のセマンティクス | 推奨される状態設計 |
|---|---|
| 別のエンティティは完全に新しいフォームを受け取る必要がある | フォームサブツリーの key としてエンティティ ID を使用する |
| エンティティに戻ったときにその下書きを復元する必要がある | 識別性の key を維持し、下書きをリフトアップして ID ごとに保存する |
| ページのリフレッシュでも下書きを復元する必要がある | 有効期限とクリーンアップルールを設定して外部に永続化する |
| 残りを保持しながら1つのフィールドをリセットする | 制御された状態(controlled state)または明示的な更新を使用する。サブツリーを再マウントしない |
| Props が派生した表示値のみを変更する | 状態にコピーするのではなく、レンダリング中に props から計算する |
key は識別性の境界を定義します。長期的な永続化を提供するものではありません。状態が drafts[userId] にリフトアップされると、 フォームサブツリーがアンマウントされても、その下書きは親に残ります。そのユーザーを再度選択すると、保存された値でフォームを初期化または制御できます。
ステップ 6: 意図しないリセットのその他の原因を認識する。
正しい key を使用していても、コンポーネントタイプを変更すると状態がリセットされます。同じツリー位置を ProfileForm から LoginPrompt に切り替えると、サブツリーが置き換えられます。
もう1つのよくある間違いは、コンポーネントを別のコンポーネントの内部で定義することです:
function ProfilePage() {
function ProfileForm() {
const [name, setName] = useState('')
return <input value={name} onChange={(event) => setName(event.target.value)} />
}
return <ProfileForm />
}ProfilePage のレンダリングごとに、新しい ProfileForm 関数オブジェクトが作成されます。React は異なるコンポーネントタイプと見なし、 予期せず input の状態をリセットします。コンポーネントの定義はトップレベルに保つ必要があります。key のみに焦点を当てた回答では、 この関連する識別性のバグを見落とす可能性があります。
ステップ 7: 仮想化リスト(Virtualized Lists)に同じ識別性ルールを適用する。
仮想化リストは、少数の表示スロットを繰り返し再利用します。ライブラリが itemKey を受け入れる場合は、ウィンドウのインデックスではなく、 ドメインエンティティ ID を返してください。そうしないと、スクロールや並べ替えによって、あるエンティティが別のスロットから状態を継承してしまう可能性があります。
大規模なリストの場合、さらに安全な設計として、行内の非永続的なビジネス状態を減らすことがよくあります。リストの上位にレコード ID ごとに 編集下書きを保存し、各行がそのエンティティの下書きを読み取るようにします。そうすれば、仮想化ライブラリが画面外の行をアンマウントしても、 ビジネスデータが削除されることはありません。
ステップ 8: レンダリングされたテキストだけでなく、状態の所有権を検証する。
すべてのテストについて、どの ID が状態を所有すべきかを明記してください:
| 操作 | 期待される結果 |
|---|---|
| Alice に下書きを入力し、先頭に Zoe を挿入する | 下書きは Alice のみに属したままである |
| 中間のアイテムを削除する | 他の行の展開状態および入力状態は移動しない |
| 降順に並べ替えた後、元の順序に戻す | 各行の状態は引き続きそのレコード ID に追従する |
| Alice を除外するようにフィルターをかけ、フィルターを解除する | 行のローカル状態はアンマウント後に失われる。必要に応じてリフトアップされた下書きストアから復元する |
| 同じ userId で親の再レンダリングをトリガーする | フォームのローカル状態が保持される |
| ユーザー A からユーザー B に切り替える | userId で key 設定されたフォームが完全にリセットされる |
| 一時的にランダムな key を使用して親の再レンダリングをトリガーする | 入力の消失と DOM の再作成によって障害メカニズムが実証される |
| 重複する ID を受け取る | 兄弟の key の競合を避けるために、データの境界で警告または拒否する |
テストではフォーカスや非制御入力もカバーする必要があります。誤った key を使用すると、React の状態は正常に見えても、 ブラウザが保持する DOM の値やフォーカスが誤ったレコードに移動してしまうことがあります。受け入れ基準には、単なるレンダリング回数だけでなく、 ユーザーに見えるエンティティの連続性を記述する必要があります。
質の高い模範回答
「私は key を、単にコンソールの警告を消すための属性ではなく、コンポーネントの識別性の一部として扱います。React は状態をレンダーツリーの位置に関連付けます。単一の親の下では、同じコンポーネントタイプと同じ key は通常同じ識別性を表すため、props の更新や移動があっても状態を保持しながら再レンダリングできます。変更されたタイプや key は新しい識別性を表します。React は古いサブツリーを削除し、新しいサブツリーをマウントします。
編集可能なリストでは、インデックスはユーザーではなく位置を識別します。[Alice, Bob] が key 0 と 1 を使用しており、先頭に Zoe が挿入された場合、key 0 を持つ古い行が Zoe をレンダリングする可能性があり、Alice の下書きや展開状態が Zoe の下に表示されてしまうことがあります。私は user.id を使用します。これにより、Alice がインデックス 0 からインデックス 1 に移動しても、Alice の識別性が安定して保たれます。レンダリング中に生成されたランダムな key、タイムスタンプ、または UUID は前回のレンダリングと決して一致しないため、React はコンポーネントと DOM を繰り返し再作成し、入力を失います。key は兄弟間でのみ一意である必要があり、子は key を prop として受け取りません。
詳細フォームは製品のセマンティクスに依存します。ユーザー A からユーザー B への切り替えでフォーム全体をクリアする必要がある場合は、ProfileForm の境界に key={userId} を配置し、複数の Effect でフィールドをクリアするのではなく、すべての子孫状態がまとめてリセットされるようにします。A に戻ったときに下書きを復元する必要がある場合は、userId ごとに識別性を分離したまま、userId ごとに下書きをリフトアップまたは外部に永続化します。
最後に、先頭への挿入、削除、並べ替え、フィルタリング、同一 ID での再レンダリング、異なる ID への切り替えをテストします。入力、展開状態、フォーカス、下書きがすべて意図したドメイン ID に追従することを確認します。これにより、key が単にページの更新を可能にするだけでなく、正しい識別性をモデル化していることが証明されます。」
よくある間違い
- key をパフォーマンスの最適化としてのみ説明する → 核心的な問題は子の識別性と状態の所有権です →
まず状態の不一致を説明し、その後に更新コストを説明してください。
- すべてのリストに配列インデックスを使用する → 挿入、削除、並べ替え、フィルタリングによってスロットとエンティティのマッピングが変更されます →
データからの安定した ID を使用してください。
- レンダリング中に UUID やランダムな値を生成する → key が毎回変更され、コンポーネントと DOM が再作成されます →
アイテムの作成時に ID を生成して保存してください。
- key がグローバルに一意であることを要求する → React は兄弟間での一意性を要求します →
現在の親およびリスト内での競合を評価してください。
- 子コンポーネントで
props.keyを読み取る → React は key を通常の prop として渡しません →
rowId または userId などの別の prop を渡してください。
- Effect 内で複雑なフォームをフィールドごとにクリアする → これにより古い状態がレンダリングされ、不要な再レンダリングが追加され、子孫の状態を見落とす可能性があります →
正しい識別性の境界で key を変更してください。
- 1つのフィールドをクリアするためにページ全体を再マウントする → これによりフォーカス、スクロール、無関係な状態が失われます →
key の境界を狭めるか、フィールドを明示的に更新してください。
- 安定した key がフィルタリング後も状態を保持すると仮定する → フィルターで除外されたコンポーネントはアンマウントされている可能性があります →
復元が必要な場合は、状態をリフトアップまたは永続化してください。
フォローアップの質問
フォローアップ 1: データにバックエンド ID がない場合、どの key を使用すべきですか?
ローカルレコードを作成するイベントで UUID などの ID を生成し、レコードフィールドとして保存します。その後のすべてのレンダリングで同じ ID を読み取ります。その契約が証明されている場合は、ドメインフィールドの不変で兄弟間で一意の組み合わせを使用することもできます。map レンダリング内で一時的に key を生成しないでください。
フォローアップ 2: key として配列インデックスが許容されるのはどのような場合ですか?
リストのライフタイム全体でメンバーシップと順序が固定され、挿入、削除、フィルタリング、並べ替えがなく、位置そのものが識別性である場合にのみ、比較的安全です。固定された静的表示はその条件を満たす場合があります。動的なビジネスデータは、並べ替えや編集によってインデックスの前提が即座に無効になるため、安定したエンティティ ID を使用する必要があります。
フォローアップ 3: userId が変更されたときに1つのフィールドのみをクリアする場合、フォーム全体の key を変更すべきですか?
必ずしもそうではありません。key は完全なサブツリーをリセットします。他のローカル状態を維持する必要がある場合は、制御されたフィールドを使用するか、props から直接値を導出するか、明示的なデータ更新パスで調整してください。サブツリー全体が別のエンティティを表し、そのすべてのローカル状態を最初からやり直す必要がある場合にのみ、key によるリセットを選択してください。
フォローアップ 4: key を変更した後に下書きを復元するにはどうすればよいですか?
下書きを親にリフトアップ(例: drafts[userId])するか、有効期限とクリーンアップルールを設定して外部に永続化します。異なるユーザーがローカル状態を共有しないように、userId に基づくフォーム key を維持します。新しくマウントされたフォームは、対応する保存済み下書きから初期値を読み取ります。識別性の分離と下書きの永続化は別々の責任です。
フォローアップ 5: 仮想化リストがすでに DOM を再利用している場合でも、安定した key は必要ですか?
はい。仮想化は表示されるノードの数を制御するだけであり、ドメインの識別性を再定義するものではありません。ライブラリが itemKey を公開している場合は、レコード ID を返してください。アンマウント後も保持する必要がある編集状態も、ID ごとに行の上位に配置する必要があります。そうしないと、行がウィンドウ外にスクロールしたときに失われてしまいます。