こんにちは、かつコーチです。
前回は条件分岐レンダリングのパターンを紹介しました。
今回は、Reactアプリでほぼ必ず登場する「配列データを一覧表示する」方法を扱います。
セットで必要になる key 属性についても、なぜ必要なのかをきちんと理解しておきましょう。
リスト表示とは?
map()で配列をJSXに変換する
Reactでは、配列の中身を一覧表示したいとき、JavaScriptの map() メソッドを使ってJSXの配列に変換します。
interface User {
id: number;
name: string;
}
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
users.map((user) => <li>...</li>) のように、配列の各要素をJSX要素に変換した配列を作り、それを { } の中に埋め込んで表示しています。
VueやAngularのような専用のループ構文はなく、あくまで「JavaScriptの配列操作をJSXの中で使う」だけというのがReactらしいポイントです。
なぜkey属性が必要なのか
先ほどのコード例で、<li> タグに key={user.id} という見慣れない属性が付いていることに気づいたと思います。
Reactは、リストの中身が変化したとき(追加・削除・並び替え)に、どの要素が変わったのかを効率よく判断するために key を利用します。
key がないと、Reactは「配列のどの項目が、画面上のどのDOM要素に対応するのか」を正確に把握できず、意図しない挙動や無駄な再描画につながることがあります。
基本の書き方
map()とkeyのセットが基本形
map() でリストを描画するときは、必ず一番外側の要素に key を指定します。
function TodoList({ todos }: { todos: { id: number; text: string }[] }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
key はコンポーネントの中で表示に使われるわけではなく、あくまでReact内部が要素を識別するための特別なPropsです。
そのため todo.key のようにコンポーネント側で参照することはできません。
keyには何を使うべきか
key に指定する値は、そのリストの中で一意な値である必要があります。
一般的には、データベースのIDなど、要素ごとに変わらない一意な値を使うのが基本です。
// 良い例:データが持つ一意なIDを使う
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
APIから取得したデータであれば id フィールドがそのまま使えることが多く、迷ったらまずIDを探すのが基本方針になります。
よくあるつまずきポイント:keyにindexを使ってしまう
実際にハマった並び替えバグ
IDが用意されていないデータをリスト表示するとき、key に配列のインデックス(index)を使ってしまうケースがよくあります。
私も以前、並び替え機能付きのTodoリストを作っていて、このindexを使ったことで思わぬバグに苦しんだ経験があります。
❌ Before:keyに配列のindexを使う
function TodoList({ todos }: { todos: { text: string; done: boolean }[] }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input type="checkbox" defaultChecked={todo.done} />
{todo.text}
</li>
))}
</ul>
);
}
一見動いているように見えるのですが、リストの先頭に新しいTodoを追加したり、途中の項目を削除したりすると、index の値が全項目でズレてしまいます。
その結果、チェックボックスのチェック状態が別のTodoの内容にすり替わって表示される、という奇妙なバグが発生しました。
key が変わらないまま中身のデータだけがズレてしまうため、Reactが「同じDOM要素を使い回してよい」と誤判断してしまうのが原因です。
デバッグ中は「なぜチェックした覚えのない項目にチェックが付くのか」がまったく分からず、原因特定に丸一日近くかかりました。
✅ After:データが持つ一意なIDをkeyに使う
function TodoList({
todos,
}: {
todos: { id: number; text: string; done: boolean }[];
}) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input type="checkbox" defaultChecked={todo.done} />
{todo.text}
</li>
))}
</ul>
);
}
各Todoに一意な id を持たせ、それを key に使うことで、並び替えや削除が起きても各要素とデータの対応関係が正しく保たれるようになりました。
index をkeyに使ってよいのは、リストの中身が絶対に追加・削除・並び替えされないと分かっている場合に限られます。
迷ったときは「このリストは並び替わる可能性があるか」を自問し、少しでも可能性があるなら一意なIDを用意するようにしています。
応用:一歩先の使い方
ネストしたリストにもそれぞれkeyを付ける
配列の中にさらに配列がある「ネストしたリスト」の場合も、それぞれの階層でkeyが必要です。
interface Category {
id: number;
name: string;
items: { id: number; label: string }[];
}
function CategoryList({ categories }: { categories: Category[] }) {
return (
<div>
{categories.map((category) => (
<div key={category.id}>
<h3>{category.name}</h3>
<ul>
{category.items.map((item) => (
<li key={item.id}>{item.label}</li>
))}
</ul>
</div>
))}
</div>
);
}
外側の category.id と内側の item.id は、それぞれ別のスコープの key として独立しているため、同じ数値が重複しても問題ありません(同じ配列内でだけ一意であればよいというルールです)。
filterと組み合わせて絞り込み表示する
map() の前に filter() を挟むと、条件に合うデータだけを一覧表示できます。
function ActiveTodoList({
todos,
}: {
todos: { id: number; text: string; done: boolean }[];
}) {
const activeTodos = todos.filter((todo) => !todo.done);
return (
<ul>
{activeTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
filter() で未完了のTodoだけに絞り込んでから map() を呼ぶことで、条件分岐レンダリングとリスト表示を自然に組み合わせられます。
まとめ
この記事のポイント
- 配列データを一覧表示するには
map()を使ってJSXの配列に変換する keyはReactが各要素を正しく識別するための特別なPropsで、リストの外側要素には必須keyにはデータが持つ一意なIDを使うのが基本- 配列のindexをkeyに使うと、並び替え・追加・削除時に表示がずれるバグにつながる
filter()とmap()を組み合わせれば、条件に合うデータだけを一覧表示できる
次に読むべき記事
リスト表示ができるようになったら、次はユーザーからの入力を受け取る「フォーム」の作り方に進みます。
次回は、Reactの基本であり最初につまずきやすいポイントでもある「制御コンポーネント」を解説します。
→ 次の記事:フォームの作り方:制御コンポーネントの基本