Reactのリスト表示とkey属性 — map()でリストを描画する
はじめに
ReactでTodoリストや商品一覧のような 「データの配列をUIに表示する」 場面、必ず出てきます。
その時に必要なのが map() でのリスト描画 と key 属性。シンプルに見えて、初心者が key で躓くパターンは現場でもよく見ます。
React入門シリーズ5回目: 配列をUIにする技術 + ありがちな落とし穴を整理します。
1. 一番シンプルな例
function FruitList() {
const fruits = ["apple", "banana", "cherry"];
return (
<ul>
{fruits.map(fruit => (
<li>{fruit}</li>
))}
</ul>
);
}
これで <ul><li>apple</li><li>banana</li><li>cherry</li></ul> が描画されます。
ポイント:
- JSX 内で
{}の中に式を書ける map()は配列を JSX要素の配列に変換する- 関数コンポーネントが「配列をそのまま返してもReactが並べてくれる」
ただし、これで実行すると コンソールに警告が出ます:
Warning: Each child in a list should have a unique "key" prop.
2. key 属性を付ける
function FruitList() {
const fruits = ["apple", "banana", "cherry"];
return (
<ul>
{fruits.map(fruit => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}
key={fruit} を付けるだけで警告は消えます。
なぜ key が必要なのか
React は 「変わった部分だけ再描画する」 ことでパフォーマンスを稼いでいます。
リストを再描画する時、「どの要素が以前のどの要素と同じか」を判断するのに key を使う のです。
-
keyが同じ → 「これは前と同じ要素、更新だけする」 -
keyが違う → 「これは新しい要素、作り直す」
key がないと、React は 位置(インデックス)で同一性を判断 することになり、後述する不具合が起きます。
3. オブジェクト配列の場合
実務では文字列の配列より、オブジェクト配列 を扱うことの方が圧倒的に多いです。
type Todo = {
id: number;
text: string;
done: boolean;
};
function TodoList() {
const todos: Todo[] = [
{ id: 1, text: "牛乳を買う", done: false },
{ id: 2, text: "資料作成", done: true },
{ id: 3, text: "ジムに行く", done: false },
];
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text} {todo.done && "✅"}
</li>
))}
</ul>
);
}
key には データ固有のID を使うのが基本。todo.id のように データベースのプライマリキー相当 の値が理想です。
4. やりがちな落とし穴: key に配列のインデックスを使う
// ❌ 動くけど推奨されない
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
「IDがないからインデックスを使えばいいや」とやりがちですが、順序が変わったり要素が追加・削除される場合にバグります。
具体的な不具合例
例えば「先頭にTODOを追加」した時:
変更前: 変更後:
[0] 牛乳を買う [0] 新しいTODO ← 追加
[1] 資料作成 [1] 牛乳を買う
[2] ジムに行く [2] 資料作成
[3] ジムに行く
React は「key=0 の要素は前回と同じ要素」と判断します。中身は変わったのに 「以前の要素を再利用」してしまう。
その結果:
- input の入力中の値が混線する
- チェックボックスのチェック状態が別の項目に紛れる
- アニメーションがおかしくなる
「動いてるように見える」のが厄介で、本番リリース後に「TODOを編集中に他の項目に文字が反映される」みたいなトラブルになります。
いつなら index でも OK?
- リストの順番が絶対に変わらない
- 要素の追加・削除がない
- 静的に表示するだけ
つまり「実質的に固定リストの時だけ」許容、と考えると安全です。
5. IDがないデータをどう扱うか
「APIから返ってくるデータにIDがない」というケース、たまにあります。
解決策1: crypto.randomUUID() で生成
const todosWithId = todos.map(todo => ({
...todo,
id: crypto.randomUUID(),
}));
ただし 毎回違うIDになるので、useState や useMemo と組み合わせて 「最初の1回だけ生成」 する工夫が必要です。
解決策2: データ内容を組み合わせて一意なキーを作る
{posts.map(post => (
<article key={`${post.date}-${post.title}`}>...</article>
))}
複数フィールドを組み合わせて一意性を担保する。重複しないことを保証できるなら これで OK。
6. <> (Fragment) と key
複数要素を返したい時、<> で囲むのが定番ですが、key を付けたい場合は省略形が使えません。
// ❌ 省略形では key を付けられない
{items.map(item => (
<>
<dt key={item.id}>{item.label}</dt> // ← 親に付けるべき
<dd>{item.value}</dd>
</>
))}
// ✅ 明示的に Fragment を使う
import { Fragment } from "react";
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.label}</dt>
<dd>{item.value}</dd>
</Fragment>
))}
地味ですが、<dl> 内で <dt>/<dd> を組で出したい時などに使います。
7. 実例: 削除可能なTODOリスト
useState と組み合わせて、削除機能を付けてみます。
import { useState } from "react";
type Todo = { id: number; text: string };
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: "牛乳を買う" },
{ id: 2, text: "資料作成" },
{ id: 3, text: "ジムに行く" },
]);
const remove = (id: number) => {
setTodos(prev => prev.filter(t => t.id !== id));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => remove(todo.id)}>削除</button>
</li>
))}
</ul>
);
}
-
key={todo.id}で各要素を識別 - 削除しても 残った要素の key は変わらない → React が正しく差分検出してくれる
これが index を key にしていると、削除後に 全部の要素が「別物」と判定されて再生成 されることがあり、無駄な再描画とバグの温床になります。
まとめ
| 項目 | ポイント |
|---|---|
| リスト描画 |
配列.map(item => <Tag>{item}</Tag>) で書ける |
key の役割 |
Reactが「どの要素が変わったか」を判断する目印 |
key に使う値 |
データ固有のID(DBの主キー等) |
| index を使う罠 | 順序変化・追加削除がある場合は使わない |
| Fragment と key | 省略形 <> では付けられない、<Fragment key=...> を使う |
おわりに
key は警告を消すための「おまじない」ではなく、Reactが効率よく差分検出するための重要な情報 です。
特に index を key にする罠は、動いてるように見えてバグが潜む タイプの問題で、現場で詰む人が後を絶ちません。
「IDがあるなら必ず使う、ないなら作る」これだけ徹底しておけば、リスト描画で困ることはほとんど無くなります。
次回は useContext で、props のバケツリレーから卒業する話を書きます。