1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactのリスト表示とkey属性 — map()でリストを描画する

1
Posted at

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 のバケツリレーから卒業する話を書きます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?