【React】keyとは何か?なぜ必要なのか?(エラー/警告対処法と原因を実例で解説:Warning: Each child in a list should have a unique “key” prop)

react-prograshi(プロぐらし)-kv React.js
記事内に広告が含まれていることがあります。

Reactでは、mapやreduceなどを使うたびにkeyが求められます。なんとなくitem.idを設定しているけれど、「じゃあ、それがないと何が悪いの?」と疑問に思ったことはありませんか?

keyの有無は、見かけ上は動くアプリと、ユーザー体験を損なうバグが潜むアプリとの境界線です。

この記事では、なぜkeyが必要なのか、そしてどんな値を使ってはいけないのか、keyを使う際の注意点などを実例を交えて解説しています。


Reactのkeyとは何か?

Reactのkeyとは、key は React が「どの要素が以前のレンダーと同じものか」を判断するための識別子です。Reactが正しく高速な表示を行うための重要な機能です。

Reactは、画面の表示内容(DOMツリー)を効率よく更新するために、「仮想DOM(Virtual DOM)」という仕組みを使っています。

仮想DOMとは、実際のDOMの軽量なコピーのようなもので、Reactはまずこの仮想DOM上で変更を加え、変更前後の仮想DOMを比較(差分検知(diffing)と呼びます)し、本当に変更が必要な部分だけを実際のDOMに反映します。これにより、高速な描画を実現しています。

この差分検知の際に、リストなどの複数の要素を処理するとき、Reactはどの要素が「変更された」「追加された」「削除された」のかを特定する必要があります。その際に必要なのがkeyです

keyの役割

keyの役割は①要素の識別、②効率的な更新です。

  • 要素の識別: リスト内の各要素に安定した一意な識別子(ID)を与えます。
  • 効率的な更新: Reactはkeyを使って、リストの要素の並びが変わったり、追加・削除があったりした際に、変更があった要素だけを正確かつ効率的に更新します。これにより、無駄な再レンダリングを減らし、パフォーマンスを向上させます。



keyが必要となるメソッド

Reactにおいてkeyプロパティが必要になる主要なメソッドは、ネストしたオブジェクトから複数のReact要素を生成するメソッドです。

その中でも、実際にkeyを使うことが多いのは以下のメソッドを使う場合です。

keyを必要とする主なメソッド
  1. map: 配列の各要素を変換し、新しい配列を生成する。
  2. filter&map:配列から特定の条件を満たす要素を抽出する。
  3. reduce:配列の要素を順次処理し、単一の累計値に集約する。


なお、keyがないと、「Warning: Each child in a list should have a unique “key” prop」という警告が出ます。Reactでエラーやバグなく、正しく・早く表示させるためにもkeyは必ずつける必要があります。


keyの使い方

keyは、配列の反復処理(mapやfilter.mapなど)によって生成される複数のReact要素の配列において、各要素の最上位(ルート)に一度だけ記述します

keyは必ず固有の値で他と被らないものである必要があります。

<タグ key='固有の値'></タグ>


通常、配列やオブジェクトが保有するidなどの固有な番号を使います。例えば、mapを使い、複数のliタグを生成する場合は以下のようにします。

const items = [
  { id: 1, text: 'りんご' },
  { id: 2, text: 'バナナ' },
  { id: 3, text: 'みかん' }
];

function ItemList() {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}> 
          {item.text}
        </li>
      ))}
    </ul>
  );
}

ここで、最上位のliタグの中で以下のようにkey属性を指定していることがポイントです。

<li key={item.id}> 


keyの注意点

keyを使う際は以下の点に注意する必要があります。

keyの注意点
  1. 各要素の最上位(ルート)に一度だけ記述する
  2. 他と被らず(一意)でタグの中身が変わっても変わらない安定した値を使う
  3. 配列のインデックスは使わない
  4. keyは子コンポーネント内で受け取れない(undefinedになる)
  5. フラグメントにkeyをつける場合はタグ名として「Fragment」が必要


各要素の最上位(ルート)に一度だけ記述する

上述しましたが、keyは各要素の最上位(ルート)に一度だけ記述する必要があります。

内側につけたkeyは機能しません。

//警告が出る
{items.map(item => (
  <li>
    <span key={item.id}>{item.text}</span> // この key は意味がない(兄弟要素の識別に使われない)
  </li>
))}

 ↓ 正しくは

{items.map(item => (
  <li key={item.id}> //各要素の最上位(ルート)に一度だけ記述する
    <span>{item.text}</span>
  </li>
))}



他と被らず(一意)でタグの中身が変わっても変わらない安定した値を使う

keyは兄弟要素(同じ配列内)で一意である必要があります。

つまり、ページに別のリストがあって同じ id を使っていても問題ありませんが、map内などの同じ配列で他と被らずユニークである必要があります。

keyの値がユニークでなければいけない理由

Math.randomや毎回生成するUUIDなど、毎回異なるkeyを与えるとReactはレンダー毎に全要素を「別物」とみなして完全に再作成してしまいます。これにより、パフォーマンス低下したり状態が失われてしまいます。

また、keyが重複するとReactが要素を正しく識別できなくなり、バグやパフォーマンスの低下を引き起こします。

補足

この性質を利用すると、コンポーネントを完全にリセットしたい(DOMを完全に再生成したい)場合は、keyを変えます。keyが変わると、React はその要素を捨てて新しく作り直します。


配列のインデックスは使わない

配列の一意な値と聞くとインデックス番号を思い浮かべるかもしれません。ですが、インデックスをkeyに使うことは避けるべきです

配列に要素を追加したり、配列の要素を削除すると、各インデックスに対応する値がズレます。レンダリングした際に表示は移動して見えますが、DOMは再利用されているため入力とデータがズレることがあります。

{items.map((item, index) => (
  <li key={index}>{item.text}</li> //インデックスをkeyに使っている
))}
注意点

リストが静的で、並び替え、追加、削除が絶対に発生しないことが保証されている場合に限り、例外的にインデックスをkeyとして使用しても大きな問題は起こりません。

このため、インデックスをkeyに使うことでエラーになることはありませんが、警告が表示されます。実際にリストの並び替え、追加、削除を実行したときに予期せぬ挙動をします。

将来的に動的になる可能性を考慮すると、常に一意なIDを使う方が安全です。


keyは子コンポーネント内で受け取れない

keyはリストの識別子として機能するため、レンダリングされた要素や子コンポーネントが直接利用することはできません。

keyは、Reactが内部で差分検知(diffing)のためだけに使用する、特別に予約されたプロパティです。

// 親コンポーネント
function ParentList() {
  const data = [
    { id: 'a', text: 'アップル' },
    { id: 'b', text: 'バナナ' }
  ];

  return (
    <ul>
      {data.map((item) => (
        <ItemComponent 
          key={item.id}  // keyを設定。渡せない
          itemData={item} // このデータはPropsで渡せる
        />
      ))}
    </ul>
  );
}

これを、子コンポーネントのItemComponentで、props.keyとするとundefinedになります

// 子コンポーネント
function ItemComponent(props) {
  const keyAccess = props.key; //※undefinedになる!
  const dataId = props.itemData.id;

  return (
    <li>
      <p>key の値: {keyAccess ? keyAccess : 'アクセスできません (undefined)'}</p>
      <p>データID: {dataId}</p>
      <p>テキスト: {props.itemData.text}</p>
    </li>
  );
}


子コンポーネントで受け取りたい場合は、別途uniqueId={item.id}のようにPropsを指定する必要があります。

  return (
    <ul>
      {data.map((item) => (
        <ItemComponent 
          key={item.id}  //※keyを設定。渡せない
          uniqueId={item.id} //〇Props用に定義
          itemData={item} // このデータはPropsで渡せる
        />
      ))}
    </ul>
  );
}
keyがundefinedになる理由

Reactがレンダリング前に key を取り除いてしまうため、props.key にアクセスしようとすると ‘undefined’ になります。

このため、子コンポーネントは自分がリストの何番目の要素であるか、どのようなIDを持っているかを直接知ることはできません。


forやforEachはkey不要

keyは、JSX内で配列を展開して複数の要素を一度に描画するときに必要になります。

forループやforEach, filterメソッドはReact要素の配列を直接生成しないため対象外です。


forEach

forEachは、配列の各要素に対して処理を実行するだけで、新しい配列を返しません。(戻り値は常にundefined

このため、forEachを使ってReactコンポーネントを生成しようとしても、直接JSXの戻り値として使うことはできず、結果的にReact要素のリストを生成できません。

//意図した結果にならない
function ItemList({ data }) {
  return (
    <ul>
      {
        data.forEach((item) => {
          return <li key={item.id}>{item.name}</li>; // 何も返さないため、この場所には何もレンダリングされない
        })
      }
    </ul>
  )
}


for

forループは、指定された回数だけコードブロックを繰り返し実行する制御構造です。

forループも、新しい配列を返すわけではありません。このため、Reactのコンポーネントのreturn文(JSX)の内部で直接forループを使用することはできません

もし、forループを使ってReactのリストを作成する場合は、以下のように一旦空の配列に変数を定義し、forループで要素を配列にpushしてから、その配列をJSX内で展開するという方法を取る必要があります。

//リストを生成するためにforループを使う場合
function ItemList({ data }) {
  const listItems = [];
  for (let i = 0; i < data.length; i++) {
    listItems.push(<li key={data[i].id}>{data[i].name}</li>);  // 要素を生成し、keyを指定して配列に追加
  }

  return (
    <ul>
      {listItems} //別途returnでReactの要素を生成する。
    </ul>
  );
}

上記は煩雑なため、シンプルにmapを使うことが推奨です。



keyの実例(応用編)

filter&mapの例

mapはfilterメソッドと併せて使うことが多いです。

なお、filterメソッド自体は、データ配列を絞り込むことが主な役割で、直接React要素(JSX)を生成して返すわけではないため、keyは不要です。

keyが必要になるのは、そのfilterの結果を使ってさらにmapを実行し、複数のJSX要素の配列を生成したときです。

const tasks = [
  { id: 101, name: '牛乳を買う', isCompleted: false },
  { id: 102, name: '報告書を提出', isCompleted: true },
  { id: 103, name: 'Reactの学習', isCompleted: true },
  { id: 104, name: 'メール返信', isCompleted: false }
];

function TaskCategorizer({ tasks }) {
  // 1. filterによる完了データの絞り込み
  const completedTasks = tasks.filter((task) => task.isCompleted);

  // 2. filterによる未完了データの絞り込み
  const incompleteTasks = tasks.filter((task) => !task.isCompleted);

  return (
    <div>
      {/* 絞り込んだデータ配列の個数を表示するなどに利用 */}
      <p>完了したタスク数: {completedTasks.length} 件</p>
      <p>未完了のタスク数: {incompleteTasks.length} 件</p>

      {/* 絞り込んだデータを使って、別のコンポーネントに渡す */}
      <TaskListComponent title="完了済み" taskData={completedTasks} />
      <TaskListComponent title="未完了" taskData={incompleteTasks} />
    </div>
  );
}

/TaskListComponentでkeyが必要
function TaskListComponent({ title, taskData }) {
  return (
    <section>
      <h3>{title}</h3>
      <ul>
        {/* ★ ここで初めて map が使われ、JSX要素の配列が生成されるため、key が必要になる */}
        {taskData.map((task) => (
          <li key={task.id}>{task.name}</li>
        ))}
      </ul>
    </section>
  );
}


reduce

Reduce使ってリストを生成する場合、アキュムレータ(acc)を配列として初期化し、ループごとにReact要素をその配列に追加していきます。

Reduceを使うと例えば「商品の配列からリストを生成し、リスト全体を単一の要素として返却する」といったことができます。

const products = [
  { id: 'A01', name: 'PCモニター', price: 30000 },
  { id: 'B02', name: 'ワイヤレスマウス', price: 3500 },
  { id: 'C03', name: 'メカニカルキーボード', price: 12000 }
];

function ProductList({ products }) {
  // アキュムレータ(acc)の初期値として空の配列([])を指定
  const listItems = products.reduce((acc, product) => {
    // 累計値 (acc) に React 要素を追加していく
    acc.push(
      // key は、push する React 要素 (<li>) の最上位に設定
      <li key={product.id}>
        {product.name} - {product.price.toLocaleString()}円
      </li>
    );
    // 更新された acc (React要素の配列) を次の処理に渡す
    return acc;
  }, []); // アキュムレータの初期値

  return (
    <div>
      <h2>商品一覧</h2>
      <ul>
        {/* 最終的に生成された React要素の配列を展開 */}
        {listItems}
      </ul>
    </div>
  );
}


タイトルとURLをコピーしました