ReactとVueにおけるリストレンダリングのキープロパティの理解
Min-jun Kim
Dev Intern · Leapcell

はじめに
現代のフロントエンド開発において、動的なデータリストを効率的にレンダリングすることは、応答性が高くユーザーフレンドリーなアプリケーションを構築するための基盤となります。ソーシャルメディアのフィード、ショッピングカート、ユーザーデータテーブルなどを想像してみてください。これらはすべてリストに大きく依存しています。
しかし、レンダリングエンジンがリスト内の変更を識別するのを助ける適切なメカニズムなしでは、パフォーマンスが低下し、状態管理に関連する微妙なバグが発生する可能性があります。
まさにここで、ReactやVueのようなフレームワークのkeyプロパティが登場します。これは、差分アルゴリズムに重要な情報を提供し、UIをインテリジェントに更新することを可能にし、すべてを最初から再レンダリングするのではなく、各アイテムを賢く更新します。
その重要性と内部での動作を理解することは、最適化され堅牢なリストレンダリングコードを書きたいフロントエンド開発者にとって、最優先事項です。
差分アルゴリズムとリストの更新
keyプロパティ自体に飛び込む前に、ReactとVueがDOMを効率的に更新するために使用するコアメカニズムである「差分アルゴリズム」の概念を把握することが不可欠です。
コア用語:
- 仮想DOM (Virtual DOM): 実際のDOMの軽量なインメモリ表現。アプリケーションの状態が変更されると、フレームワークは新しい仮想DOMツリーを作成します。
- 差分検出 (Reconciliation): 新しい仮想DOMツリーと以前のツリーを比較して、何が変更されたかを識別するプロセス。
- 差分アルゴリズム (Diffing Algorithm): 差分検出中に、実際のDOMを更新するために必要な最小限の変更セットを決定するために使用される特定のアルゴリズム。
- DOM操作 (DOM Manipulation): ブラウザのDocument Object Modelを直接変更するコストのかかるプロセス。差分検出エンジンの目標は、これを最小限に抑えることです。
keyプロパティなしでリストをレンダリングする場合、ReactとVueは単純で素朴な比較を行います。アイテムが追加、削除、または並べ替えられた場合、デフォルトの動作は配列内の位置によって要素を比較することです。
これは非効率的な更新につながり、さらに重要なこととして、入力フィールドの値やフォーカスのような内部コンポーネントの状態の損失につながる可能性があります。
keyプロパティの役割
keyプロパティは、リスト内の各アイテムに安定した識別子を提供します。ReactまたはVueがkeyプロパティを持つ要素を見ると、その差分検出アルゴリズムはこれらのキーを使用して、古いリストのアイテムを新しいリストのアイテムに一致させます。
仕組み:
- 安定した識別子:
keyは、各リストアイテムの一意で安定した識別子であるべきです。理想的には、これはデータ自体から取得されます(例:データベースID)。 - 効率的な差分検出: リストが更新されると、差分検出エンジンはまず要素の
keyを比較します。- 新しいリストに
keyが存在し、古いリストに存在しない場合、新しいコンポーネント/要素が作成されます。 - 古いリストに
keyが存在し、新しいリストに存在しない場合、対応するコンポーネント/要素は破棄されます。 keyが両方のリストに存在する場合、コンポーネント/要素は(位置が変更された場合)移動されるか、(プロパティが変更された場合)更新されるか、最初から再レンダリングされることはありません。その内部状態は保持されます。
- 新しいリストに
一意で安定したキーが重要な理由
- 一意性: キーは、同じリスト内の兄弟間で一意でなければなりません。重複したキーは、エンジンがそれらの要素を一意に識別できないため、予測不能な動作や潜在的なバグにつながる可能性があります。
- 安定性: キーは、再レンダリング間で同じアイテムに対して一貫している必要があります。アイテムが追加、削除、または並べ替えられる可能性がある場合、特に
indexをキーとして使用することは一般的に推奨されません。indexをキーとして使用する問題: リストの先頭にアイテムを挿入すると、後続のすべてのアイテムのインデックスがシフトします。差分検出エンジンは、既存のアイテムのすべてのキー(インデックス)が変更されたと認識し、実質的にほとんどまたはすべてのリストアイテムの再レンダリングにつながり、それらの内部状態を失わせます。
React(概念はVueにも同様に適用されます)の簡単な例で説明しましょう。
シナリオ1:indexをキーとして使用(問題あり)
先頭に新しいタスクを追加できるタスクリストを検討してください。
import React, { useState } from 'react'; function TaskListIndexKey() { const [tasks, setTasks] = useState([ { id: 1, text: 'Learn React', completed: false }, { id: 2, text: 'Build a project', completed: false }, ]); const [newTaskText, setNewTaskText] = useState(''); const addTask = () => { if (newTaskText.trim() === '') return; const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1; setTasks([{ id: newId, text: newTaskText, completed: false }, ...tasks]); // 先頭に追加 setNewTaskText(''); }; return ( <div> <input type="text" value={newTaskText} onChange={(e) => setNewTaskText(e.target.value)} placeholder="New task" /> <button onClick={addTask}>Add Task</button> <ul> {tasks.map((task, index) => ( // 問題:indexをキーとして使用 <li key={index}> {task.text} <input type="checkbox" checked={task.completed} onChange={() => { /* トグルロジック */ }} /> </li> ))} </ul> </div> ); } export default TaskListIndexKey;
新しいタスク「Read a book」を先頭に追加した場合:
- 「Read a book」(新規)は
key=0を取得します。 - 「Learn React」(元の
key=0)は、key=1を取得します。 - 「Build a project」(元の
key=1)は、key=2を取得します。
差分検出エンジンは、key=0(元の「Learn React」)を持つ要素が失われ、新しい要素が出現したことを見ています。次に、key=1が新しいコンテンツを取得し、key=2も同様であると見ています。基本的に、各既存アイテムを異なる位置にある新しいアイテム、またはコンテンツが変更されたアイテムとして扱い、それらの内部状態(チェックボックスのチェック状態など)を失う可能性があります。
シナリオ2:安定した一意のIDをキーとして使用(正しいアプローチ)
データからの適切なidを使用します。
import React, { useState } from 'react'; function TaskListCorrectKey() { const [tasks, setTasks] = useState([ { id: 1, text: 'Learn React', completed: false }, { id: 2, text: 'Build a project', completed: false }, ]); const [newTaskText, setNewTaskText] = useState(''); const addTask = () => { if (newTaskText.trim() === '') return; const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1; setTasks([{ id: newId, text: newTaskText, completed: false }, ...tasks]); // 先頭に追加 setNewTaskText(''); }; return ( <div> <input type="text" value={newTaskText} onChange={(e) => setNewTaskText(e.target.value)} placeholder="New task" /> <button onClick={addTask}>Add Task</button> <ul> {tasks.map((task) => ( // 正しい:安定した一意のIDをキーとして使用 <li key={task.id}> {task.text} <input type="checkbox" checked={task.completed} onChange={() => { /* トグルロジック */ }} /> </li> ))} </ul> </div> ); } export default TaskListCorrectKey;
id=3を持つ「Read a book」を先頭に追加すると:
key=3(「Read a book」)を持つ新しい要素が一番上に挿入されます。key=1(「Learn React」)およびkey=2(「Build a project」)を持つ既存の要素が識別され、DOM内の新しい位置に移動されます。それらは同じコンポーネントであると認識されるため、内部状態(例:チェックボックスの状態)は保持されます。これははるかに効率的です。
アプリケーションシナリオ
keyプロパティの重要性は、あらゆる動的なリスト操作に及びます。
- アイテムの追加/削除: 新しいキーがマウントされ、存在しないキーがアンマウントされます。
- アイテムの並べ替え: 同じキーを持つ要素は、DOMで破棄・再作成することなく効率的に並べ替えられ、状態が保持されます。
- アイテムのフィルタリング: 削除に似ており、フィルタリングされたアイテムはアンマウントされ、フィルタリングされたアイテムはマウントされます。
- 動的フォーム: ユーザーが繰り返し入力フィールドを追加または削除できるフォームを想像してみてください。キーを使用すると、位置が変更されても、正しい入力フィールドの状態(その値)が維持されることが保証されます。
ベストプラクティス
- 常にデータから安定した一意のIDを使用してください: これが理想的なシナリオです。
indexをキーとして使用しないでください: リストが厳密に静的で、並べ替え、フィルタリング、または追加/削除されない限り、絶対に使用しないでください。それでも、一般的なルールとして避ける方が安全です。Math.random()または現在のタイムスタンプをキーとして使用しないでください: これらは一意の値を生成しますが、安定していません。再レンダリングごとに新しいキーが生成され、実質的にリストの完全な再レンダリングを強制し、状態を失い、キーの目的を無効にします。
結論
ReactとVueのkeyプロパティは、単なる些細な詳細ではなく、リストレンダリングの効率と正確性を支える基本的なメカニズムです。各アイテムに安定した識別子を提供することにより、差分検出エンジンがUIをインテリジェントに更新し、不要なDOM操作を防ぎ、コンポーネント状態を保持することを可能にします。
一意で安定したキーを採用することは、高性能でバグのないフロントエンドアプリケーションを構築するために不可欠です。リストアイテムには常に安定した一意の識別子を提供してください。これにより、最適なパフォーマンスとスムーズなユーザーエクスペリエンスが保証されます。

