リストのレンダー

データの集まりから、似たようなコンポーネントを複数表示させたいことがあります。データの配列を操作するには JavaScript の配列メソッドが使えます。このページでは filter()map() を React で使用して、データの配列をフィルタリングしたり、コンポーネントの配列に変換したりする方法を見ていきましょう。

このページで学ぶこと

  • JavaScript の map() を使用して、配列からコンポーネントをレンダーする方法
  • JavaScript の filter() を使用して、特定のコンポーネントのみをレンダーする方法
  • React での key の使用方法と、その必要性

配列からデータをレンダー

以下のようなコンテンツのリストがあるとしましょう。

<ul>
<li>Creola Katherine Johnson: mathematician</li>
<li>Mario José Molina-Pasquel Henríquez: chemist</li>
<li>Mohammad Abdus Salam: physicist</li>
<li>Percy Lavon Julian: chemist</li>
<li>Subrahmanyan Chandrasekhar: astrophysicist</li>
</ul>

これらのリスト項目は、その中身、すなわちデータのみが異なっています。インターフェースを構築する際にはよく、コメント一覧やプロフィール画像のギャラリなどのように、異なるデータを使用して同じコンポーネントの複数のインスタンスを表示する必要があります。このような場合、JavaScript のオブジェクトや配列にそのデータを保存し、map()filter() などのメソッドを使ってコンポーネントのリストをレンダーすることができます。

以下は、配列からアイテムのリストを生成する方法を示す簡単な例です。

  1. データを配列に移動します。
const people = [
'Creola Katherine Johnson: mathematician',
'Mario José Molina-Pasquel Henríquez: chemist',
'Mohammad Abdus Salam: physicist',
'Percy Lavon Julian: chemist',
'Subrahmanyan Chandrasekhar: astrophysicist'
];
  1. people 内のメンバを listItems という新しい JSX の配列にマップします。
const listItems = people.map(person => <li>{person}</li>);
  1. コンポーネントから listItems<ul> で囲んで返します。
return <ul>{listItems}</ul>;

以下が結果です。

const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist',
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
];

export default function List() {
  const listItems = people.map(person =>
    <li>{person}</li>
  );
  return <ul>{listItems}</ul>;
}

上のサンドボックスには、以下のようなコンソールエラーが表示されています。

Console
Warning: Each child in a list should have a unique “key” prop.

このエラーを修正する方法についてはこのページの後半で説明します。その前に、このデータに構造を追加しましょう。

アイテムの配列をフィルタする

このデータにさらに構造を加えてみました。

const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
}, {
name: 'Percy Lavon Julian',
profession: 'chemist',
}, {
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
}];

ここで、職業 (profession) が 'chemist' の人だけを表示したいとしましょう。JavaScript の filter() メソッドを使用すれば、そのような職業の人だけを返すことができます。このメソッドは、要素の配列を受け取り、個々の要素を「テスト」(true または false を返す関数)にかけ、そして、テストを通過した要素(テスト関数が true を返したもの)のみを含む配列を返します。

profession'chemist' となっている要素のみが必要でした。そのための「テスト」関数は、(person) => person.profession === 'chemist' のようになります。使い方の全体像は以下のようになります。

  1. people に対して filter() を呼び出し、person.profession === 'chemist' を使ってフィルタした、職業が “chemist” である人物のみの新しい配列を作成します。
const chemists = people.filter(person =>
person.profession === 'chemist'
);
  1. 次に chemists に対して map を適用します。
const listItems = chemists.map(person =>
<li>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
  1. 最後に、コンポーネントから listItems返します。
return <ul>{listItems}</ul>;
import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const chemists = people.filter(person =>
    person.profession === 'chemist'
  );
  const listItems = chemists.map(person =>
    <li>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return <ul>{listItems}</ul>;
}

落とし穴

アロー関数は => の直後の式を自動的に返しますので、return 文を直接書く必要はありません。

const listItems = chemists.map(person =>
<li>...</li> // Implicit return!
);

ただし、もし => の次に { が続く場合は、必ず return 文を明示的に書く必要があります

const listItems = chemists.map(person => { // Curly brace
return <li>...</li>;
});

=> { を含むアロー関数は “ブロック形式の関数本体” を持つものとして扱われます。これにより複数行のコードが書けるようになりますが、return 文を自分で書かなければなりません。書き忘れた場合は何も返されません!

key によるリストアイテムの順序の保持

上記のすべてのサンドボックスで、コンソールにエラーが表示されていることに注目しましょう:

Console
Warning: Each child in a list should have a unique “key” prop.

配列の各アイテムには、key を渡す必要があります。配列内の他のアイテムと区別できるようにするための一意な文字列ないし数値のことです。

<li key={person.id}>...</li>

補足

map() 内で直接 JSX 要素を使用する場合、必ず key が必要です!

key は、配列のどの要素がどのコンポーネントに対応するのかを React が判断し、後で正しく更新するために必要です。これが重要となるのは、配列の要素が移動(ソートなどによって)した場合、挿入された場合、あるいは削除された場合です。適切に key を選ぶことで、React は何が起こったか推測し、DOM ツリーに正しい更新を反映させることができます。

key は動的に生成するのではなく、元データに含めるべきです。

export const people = [{
  id: 0, // Used in JSX as a key
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1, // Used in JSX as a key
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2, // Used in JSX as a key
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3, // Used in JSX as a key
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4, // Used in JSX as a key
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];

さらに深く知る

リストアイテムごとに複数の DOM ノードを表示する

各アイテムが 1 つの DOM ノードではなく、複数の DOM ノードをレンダーする必要がある場合はどうするのでしょうか?

短い <>...</> フラグメント構文では key を渡せないため、これらを 1 つの <div> にグループ化するか、やや長くてより明示的な <Fragment> 構文を使用する必要があります。

import { Fragment } from 'react';

// ...

const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);

フラグメントは DOM から消え去るため、これにより <h1><p><h1><p> というように続くフラットなリストが生成されます。

key をどこから得るのか

データソースの種類によって key を得る方法は異なります。

  • データベースからのデータ: データがデータベースから来る場合、データベースのキーや ID は必然的に一意ですので、それを利用できます。
  • ローカルで生成されたデータ: データがローカルで生成されて保持される場合(例:ノートを取るアプリにおけるノート)は、アイテムを作成する際に、インクリメンタルなカウンタや crypto.randomUUID()、または uuid などのパッケージを使用します。

key のルール

  • キーは兄弟間で一意でなければなりません。ただし、異なる配列に対応する JSX ノードには同じキーを使用することができます。
  • キーは変更してはいけません。さもないと key の目的が台無しになります。レンダーの最中に key を生成してはいけません。

なぜ React は key を必要とするのか

デスクトップ上のファイルに名前がない場合を想像してください。代わりに、最初のファイル、2 番目のファイルといったように、順番によってそれらを区別する必要があるとしましょう。そのうち番号に慣れるかもしれませんが、ファイルを削除した途端に混乱してしまいますね。2 番目のファイルが 1 番目のファイルになり、3 番目のファイルが 2 番目のファイルになり、という具合です。

フォルダ内のファイル名と JSX の key の目的は似ています。兄弟間で項目を一意に識別できるようにするのです。適切に選択された key は、配列内の位置よりも多くの情報を提供します。並べ替えによって位置が変更されたとしても、key のおかげで React はその項目が存在する限り、それを一意に識別できるのです。

落とし穴

アイテムのインデックスを key として使用したくなるかもしれません。実際、key を指定しなかった場合、React はデフォルトでインデックスを使用します。しかし、アイテムが挿入されたり削除されたり、配列を並び替えたりすると、レンダーするアイテムの順序も変わります。インデックスをキーとして利用すると、微妙かつややこしいバグの原因となります。

同様に、key={Math.random()} などとしてキーをその場で生成してはいけません。こうするとキーがレンダーごとに一切合致しなくなり、コンポーネントと DOM が毎回再作成されるようになります。これは遅くなるのみならず、リストアイテム内のユーザによる入力値も失われてしまいます。代わりに、データに紐づいた安定した ID を使用してください。

コンポーネントは key を props として受け取らないということに注意してください。React 自体がヒントとして使用するだけです。コンポーネントが ID を必要とする場合は、別の props として渡す必要があります:<Profile key={id} userId={id} />

まとめ

このページでは以下のことを学びました。

  • コンポーネントからデータを配列やオブジェクトといったデータ構造に移動する方法。
  • JavaScript の map() を使用して類似したコンポーネントの集まりを作成する方法。
  • JavaScript の filter() を使用してフィルタリングされたアイテムの配列を作成する方法。
  • コレクション内の各コンポーネントに key を設定して、位置やデータが変更された場合でも React がそれぞれを追跡できるようにする方法と、それが必要な理由。

チャレンジ 1/4:
リストを 2 つに分割

この例では、すべての人物の一覧を表示しています。

それを、化学者 (chemist)その他の人 の 2 つの別々の一覧に変更してください。前に述べたように、person.profession === 'chemist' であるかどうかを確認することで、その人が化学者であるかどうかを決定できます。

import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const listItems = people.map(person =>
    <li key={person.id}>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return (
    <article>
      <h1>Scientists</h1>
      <ul>{listItems}</ul>
    </article>
  );
}