React学習記録

【React】map関数でリストをレンダリングする方法とkeyの重要性

yuya_react

はじめに

こんにちは、優也@月収100万を目指して月100時間勉強する男です。

これまでのシリーズで、「useState」でデータを管理し、「イベントハンドラ」でユーザー操作に対応する方法を学びました。今回は基礎編の最終回として、map関数を使ったリストのレンダリングについて解説します。

map関数は、配列のデータを画面にリスト表示するための必須テクニックです。TODOアプリのタスク一覧、商品リスト、ユーザーリストなど、あらゆる場面で使います。

この記事を読めば、Reactでインタラクティブなアプリを作るための基礎知識がすべて揃います。次回からは、いよいよ実際にTODOアプリを作っていきます!

map関数とは?

map関数は、JavaScriptの配列メソッドで、配列の各要素に対して処理を行い、新しい配列を返します。

JavaScript の map関数(おさらい)

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((num) => num * 2);

console.log(doubled); // [2, 4, 6, 8, 10]

各要素を2倍にした新しい配列が作られます。

React での map関数

Reactでは、配列の各要素をJSX要素に変換して表示します:

const numbers = [1, 2, 3, 4, 5];

return (
  <ul>
    {numbers.map((num) => (
      <li>{num}</li>
    ))}
  </ul>
);

これで、1から5までのリストが画面に表示されます。

基本的な使い方

シンプルなリスト表示

import { useState } from 'react';

function App() {
  const fruits = ['りんご', 'バナナ', 'オレンジ', 'ぶどう'];

  return (
    <div>
      <h1>果物リスト</h1>
      <ul>
        {fruits.map((fruit) => (
          <li>{fruit}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

処理の流れ

  1. fruits配列の各要素に対してmap関数が実行される
  2. 各要素(fruit)が<li>{fruit}</li>に変換される
  3. すべての<li>要素がまとめて表示される

{}で囲む理由

javascript

<ul>
  {fruits.map((fruit) => (
    <li>{fruit}</li>
  ))}
</ul>
```

JSXの中でJavaScriptの式を使うには`{}`で囲む必要があります。

## keyの重要性

上記のコードを実行すると、コンソールに以下の警告が表示されます:
```
Warning: Each child in a list should have a unique "key" prop.

なぜkeyが必要?

Reactは、リストの各要素を効率的に更新するためにkeyを使います。keyがないと:

  • どの要素が変更されたか判断できない
  • リスト全体を再レンダリングしてしまう
  • パフォーマンスが悪化する

keyを追加する

import { useState } from 'react';

function App() {
  const fruits = ['りんご', 'バナナ', 'オレンジ', 'ぶどう'];

  return (
    <div>
      <h1>果物リスト</h1>
      <ul>
        {fruits.map((fruit, index) => (
          <li key={index}>{fruit}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

ポイント:

  • mapの第2引数indexを使う
  • key={index}でkeyを指定

keyに何を使うべきか?

❌ 避けるべき: index

{items.map((item, index) => (
  <li key={index}>{item}</li>
))}

リストの順序が変わる場合、indexは適切なkeyではありません。

✅ 推奨: 一意のID

{items.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

データに一意のIDがある場合は、それを使います。

なぜindexは避けるべき?

例えば、リストの先頭に要素を追加した場合:

<em>// 元のリスト</em>
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
<em>// index: 0='A', 1='B'</em>

<em>// 先頭に追加</em>
[{ id: 3, name: 'C' }, { id: 1, name: 'A' }, { id: 2, name: 'B' }]
<em>// index: 0='C', 1='A', 2='B'</em>

indexが変わってしまうため、Reactが正しく要素を識別できません。

ただし、以下の場合はindexでもOK:

  • リストが静的(追加・削除・並び替えがない)
  • 表示のみで操作しない

オブジェクトの配列を表示

実際のアプリでは、オブジェクトの配列を扱うことが多いです。

基本形

import { useState } from 'react';

function App() {
  const users = [
    { id: 1, name: '田中太郎', age: 25 },
    { id: 2, name: '佐藤花子', age: 30 },
    { id: 3, name: '鈴木次郎', age: 28 }
  ];

  return (
    <div>
      <h1>ユーザーリスト</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} ({user.age}歳)
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

テーブル形式で表示

import { useState } from 'react';

function App() {
  const users = [
    { id: 1, name: '田中太郎', age: 25, city: '東京' },
    { id: 2, name: '佐藤花子', age: 30, city: '大阪' },
    { id: 3, name: '鈴木次郎', age: 28, city: '福岡' }
  ];

  return (
    <div>
      <h1>ユーザーリスト</h1>
      <table border="1">
        <thead>
          <tr>
            <th>名前</th>
            <th>年齢</th>
            <th>都市</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.age}</td>
              <td>{user.city}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default App;

stateと組み合わせる

動的にリストを更新する場合は、stateを使います。

import { useState } from 'react';

function App() {
  const [items, setItems] = useState([
    { id: 1, name: 'りんご' },
    { id: 2, name: 'バナナ' },
    { id: 3, name: 'オレンジ' }
  ]);

  const addItem = () => {
    const newId = items.length > 0 
      ? Math.max(...items.map(item => item.id)) + 1 
      : 1;
    
    setItems([...items, { id: newId, name: 'ぶどう' }]);
  };

  const deleteItem = (id) => {
    setItems(items.filter(item => item.id !== id));
  };

  return (
    <div>
      <h1>果物リスト</h1>
      <button onClick={addItem}>追加</button>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name}
            <button onClick={() => deleteItem(item.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

このコードでは:

  • addItem: 新しい要素を配列に追加
  • deleteItem: 指定したIDの要素を削除
  • key={item.id}: 一意のIDを使用

条件付きレンダリングと組み合わせる

特定の条件に合う要素だけを表示することもできます。

filter と map の組み合わせ

import { useState } from 'react';

function App() {
  const [users] = useState([
    { id: 1, name: '田中太郎', age: 25, isActive: true },
    { id: 2, name: '佐藤花子', age: 30, isActive: false },
    { id: 3, name: '鈴木次郎', age: 28, isActive: true }
  ]);

  return (
    <div>
      <h1>アクティブユーザー</h1>
      <ul>
        {users
          .filter(user => user.isActive)
          .map((user) => (
            <li key={user.id}>
              {user.name} ({user.age}歳)
            </li>
          ))}
      </ul>
    </div>
  );
}

export default App;

空のリストの場合の表示

import { useState } from 'react';

function App() {
  const [items] = useState([]);

  return (
    <div>
      <h1>タスクリスト</h1>
      {items.length === 0 ? (
        <p>タスクがありません</p>
      ) : (
        <ul>
          {items.map((item) => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;

TODOアプリでの実例

実際のTODOアプリでは、このようにmap関数を使います:

import { useState } from 'react';

function App() {
  const [inputTask, setInputTask] = useState('');
  const [tasks, setTasks] = useState([
    { id: 1, taskName: '朝食を作る', completed: false },
    { id: 2, taskName: '買い物に行く', completed: false },
    { id: 3, taskName: 'ブログを書く', completed: true }
  ]);

  const addTask = () => {
    if (inputTask.trim() === '') return;
    
    const newId = tasks.length > 0 
      ? Math.max(...tasks.map(task => task.id)) + 1 
      : 1;
    
    setTasks([...tasks, { id: newId, taskName: inputTask, completed: false }]);
    setInputTask('');
  };

  const deleteTask = (id) => {
    setTasks(tasks.filter(task => task.id !== id));
  };

  const toggleComplete = (id) => {
    setTasks(tasks.map(task => 
      task.id === id 
        ? { ...task, completed: !task.completed } 
        : task
    ));
  };

  return (
    <div>
      <h1>TODOアプリ</h1>
      
      <div>
        <input 
          type="text"
          value={inputTask}
          onChange={(e) => setInputTask(e.target.value)}
          placeholder="タスクを入力"
        />
        <button onClick={addTask}>追加</button>
      </div>

      <h2>未完了タスク</h2>
      <ul>
        {tasks
          .filter(task => !task.completed)
          .map((task) => (
            <li key={task.id}>
              {task.taskName}
              <button onClick={() => toggleComplete(task.id)}>完了</button>
              <button onClick={() => deleteTask(task.id)}>削除</button>
            </li>
          ))}
      </ul>

      <h2>完了タスク</h2>
      <ul>
        {tasks
          .filter(task => task.completed)
          .map((task) => (
            <li key={task.id} style={{ textDecoration: 'line-through' }}>
              {task.taskName}
              <button onClick={() => deleteTask(task.id)}>削除</button>
            </li>
          ))}
      </ul>
    </div>
  );
}

export default App;

このコードでは:

  • map: タスクを一覧表示
  • filter + map: 未完了/完了タスクを分けて表示
  • key={task.id}: 一意のIDを使用
  • toggleComplete: mapを使ってcompletedを切り替え

ネストしたmap

リストの中にさらにリストがある場合もあります。

import { useState } from 'react';

function App() {
  const categories = [
    {
      id: 1,
      name: '果物',
      items: ['りんご', 'バナナ', 'オレンジ']
    },
    {
      id: 2,
      name: '野菜',
      items: ['にんじん', 'トマト', 'キャベツ']
    }
  ];

  return (
    <div>
      <h1>カテゴリー別リスト</h1>
      {categories.map((category) => (
        <div key={category.id}>
          <h2>{category.name}</h2>
          <ul>
            {category.items.map((item, index) => (
              <li key={index}>{item}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

export default App;

注意点:

  • 外側のmapと内側のmap、それぞれにkeyが必要
  • 内側のリストに一意のIDがない場合、indexを使うのもあり

よくある間違いと注意点

❌ 間違い1: keyを付け忘れる

// ❌ keyがない
{items.map((item) => (
  <li>{item.name}</li>
))}

// ✅ keyを付ける
{items.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

❌ 間違い2: keyに同じ値を使う

// ❌ すべて同じkeyになってしまう
{items.map((item) => (
  <li key="item">{item.name}</li>
))}

// ✅ 一意の値を使う
{items.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

❌ 間違い3: map内で直接return

// ❌ returnが必要
{items.map((item) => {
  <li key={item.id}>{item.name}</li>
})}

// ✅ ()で囲む、またはreturnを書く
{items.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

// または
{items.map((item) => {
  return <li key={item.id}>{item.name}</li>;
})}

❌ 間違い4: 複数の要素をreturn

// ❌ 複数の要素を直接returnできない
{items.map((item) => (
  <h3>{item.title}</h3>
  <p>{item.description}</p>
))}

// ✅ 1つの要素で囲む
{items.map((item) => (
  <div key={item.id}>
    <h3>{item.title}</h3>
    <p>{item.description}</p>
  </div>
))}

// または<>...</>を使う(Fragmentショートハンド)
{items.map((item) => (
  <React.Fragment key={item.id}>
    <h3>{item.title}</h3>
    <p>{item.description}</p>
  </React.Fragment>
))}

まとめ

map関数を使ったリストレンダリングについて解説しました。

重要ポイント:

  • ✅ map関数で配列を JSX 要素に変換
  • ✅ 必ずkeyを指定する
  • ✅ keyには一意の値を使う(できればID)
  • ✅ filter と組み合わせて条件付き表示
  • ✅ stateと組み合わせて動的なリスト

これで基礎知識が揃いました!

これまで学んだ:

この3つが揃えば、インタラクティブなアプリが作れます

次回からは、いよいよ実際にTODOアプリを作っていきます。環境構築から始めて、タスクの表示、追加、削除、完了機能まで、段階的に実装していきましょう!


この記事のシリーズ:

  1. 【React入門】未経験からReactを学んで最初のTODOアプリを作った話
  2. 【2025年版】Viteで始めるReact開発環境の構築方法
  3. 【React基礎】useStateとは?初心者向けに分かりやすく解説
  4. 【React】イベントハンドラの書き方完全ガイド
  5. 【React】map関数でリストをレンダリングする方法 ← 今ここ
  6. 【React入門】TODOアプリの作り方 – 前編(次回)

練習問題

理解を深めるために、以下を実装してみましょう:

  1. 好きな映画リスト: 映画のタイトルと評価(星5段階)を表示
  2. ショッピングリスト: 商品名、価格、個数を表示し、合計金額を計算
  3. フィルター機能付きリスト: 検索欄を作り、入力に応じてリストをフィルター

次回から、これらの知識を総動員してTODOアプリを作っていきます!


あとがき

月100時間の学習時間の中で、今月はReactに集中して取り組んでいます。Instagramでは、フリーランスとして月収100万円を目指す道のりを、リアルタイムで発信していきますので、ぜひフォローしてください!

Instagram: @hooded_yuya_100man

ABOUT ME
記事URLをコピーしました