Reactでmapを使った繰り返し処理|基本から実践まで徹底解説

Reactのmap関数を使ったリストレンダリングを基本から応用まで詳しく解説。key props、パフォーマンス最適化、実践的な使用例を紹介

Learning Next 運営
39 分で読めます

みなさん、Reactで配列のデータを画面に表示したいと思ったことはありませんか?

「繰り返し処理ってどう書けばいいの?」「mapって何?」と悩んだ経験はありませんか?

この記事では、Reactのmap関数を基本から実践まで詳しく解説します。 難しそうに見えますが、実は簡単に理解できるんです。

一緒にmap関数をマスターして、動的なWebアプリを作れるようになりましょう!

Map関数って何?

配列を変換する魔法の関数

Map関数は、配列の各要素を新しい形に変換するJavaScriptの関数です。

簡単に言うと、「A」という配列を「B」という別の配列に変換してくれるんです。

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

const doubledNumbers = numbers.map(number => number * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]

上のコードを見てみましょう。 numbers配列の各要素を2倍にして、新しい配列を作っています。

元の配列は変更されません。 これが非破壊的という特徴です。

ReactでのMap関数の役割

ReactでMap関数は、データの配列をJSX要素の配列に変換します。

function UserList() {
  const users = ['太郎', '花子', '次郎'];

  return (
    <ul>
      {users.map(user => (
        <li key={user}>{user}</li>
      ))}
    </ul>
  );
}

文字列の配列が、<li>要素の配列に変換されました。 これで画面にリストが表示されるんです。

実行すると、こんなHTMLが生成されます:

<ul>
  <li>太郎</li>
  <li>花子</li>
  <li>次郎</li>
</ul>

基本的なリストを作ってみよう

簡単なデータから始めて、だんだん複雑なリストを作れるようになりましょう。

文字列のリスト表示

まずは一番シンプルなパターンから。

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

  return (
    <div className="fruit-list">
      <h2>フルーツ一覧</h2>
      <ul>
        {fruits.map((fruit, index) => (
          <li key={index} className="fruit-item">
            {fruit}
          </li>
        ))}
      </ul>
    </div>
  );
}

fruits配列の各フルーツが、<li>要素として表示されます。 key={index}は、Reactが効率的に更新するために必要です。

数値データの表示

今度は数値を扱ってみましょう。

function ScoreList() {
  const scores = [85, 92, 78, 96, 88, 73, 90];

  const average = scores.reduce((sum, score) => sum + score, 0) / scores.length;
  const maxScore = Math.max(...scores);

  return (
    <div className="score-list">
      <h2>テスト結果</h2>
      
      <div className="score-stats">
        <p>平均点: {average.toFixed(1)}点</p>
        <p>最高点: {maxScore}点</p>
      </div>

      <div className="score-items">
        {scores.map((score, index) => (
          <div 
            key={index} 
            className={`score-item ${score >= 80 ? 'good' : 'needs-improvement'}`}
          >
            <span className="score-number">第{index + 1}回</span>
            <span className="score-value">{score}点</span>
            <span className="score-grade">
              {score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'D'}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

reduceで平均点を計算しています。 Math.max(...scores)で最高点を取得。

条件演算子(?:)で、点数に応じてクラス名や評価を変えています。 80点以上なら「good」、それ以下なら「needs-improvement」のクラスが適用されます。

オブジェクト配列の表示

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

function UserCard() {
  const users = [
    {
      id: 1,
      name: '田中太郎',
      email: 'tanaka@example.com',
      age: 28,
      department: '開発部',
      avatar: '/images/avatar1.jpg'
    },
    {
      id: 2,
      name: '佐藤花子',
      email: 'sato@example.com',
      age: 32,
      department: 'デザイン部',
      avatar: '/images/avatar2.jpg'
    },
    {
      id: 3,
      name: '山田次郎',
      email: 'yamada@example.com',
      age: 25,
      department: 'マーケティング部',
      avatar: '/images/avatar3.jpg'
    }
  ];

  return (
    <div className="user-cards">
      <h2>チームメンバー</h2>
      <div className="cards-grid">
        {users.map(user => (
          <div key={user.id} className="user-card">
            <div className="card-header">
              <img 
                src={user.avatar} 
                alt={user.name}
                className="user-avatar"
              />
              <div className="user-info">
                <h3 className="user-name">{user.name}</h3>
                <p className="user-department">{user.department}</p>
              </div>
            </div>
            
            <div className="card-body">
              <p className="user-email">📧 {user.email}</p>
              <p className="user-age">🎂 {user.age}歳</p>
            </div>
            
            <div className="card-actions">
              <button className="contact-button">連絡する</button>
              <button className="profile-button">詳細を見る</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

オブジェクトの各プロパティ(user.nameuser.emailなど)にアクセスして表示しています。

key={user.id}で、一意のIDをキーとして使用。 これがベストプラクティスです。

各ユーザーカードには、画像、名前、部署、連絡先、アクションボタンが含まれています。

Key Propsが超重要な理由

ReactでMap関数を使うときに絶対に必要なのがKey Propsです。

Key Propsって何?

Key Propsは、Reactが効率的にDOMを更新するための識別子です。

// ❌ 良くない例:keyが設定されていない
function BadExample() {
  const items = ['A', 'B', 'C'];
  
  return (
    <ul>
      {items.map(item => (
        <li>{item}</li> // Warning: Each child should have a unique "key" prop
      ))}
    </ul>
  );
}

コンソールに警告が表示されます。 Reactが「keyを設定してね」と教えてくれているんです。

// ✅ 良い例:適切なkeyが設定されている
function GoodExample() {
  const items = ['A', 'B', 'C'];
  
  return (
    <ul>
      {items.map((item, index) => (
        <li key={`item-${index}`}>{item}</li>
      ))}
    </ul>
  );
}

key={item-${index}}で、各要素に一意のキーを設定しました。

適切なKey値の選び方

一番良いのは、データに含まれる一意のIDを使うことです。

function UserList() {
  const users = [
    { id: 'user_001', name: '太郎', email: 'taro@example.com' },
    { id: 'user_002', name: '花子', email: 'hanako@example.com' },
    { id: 'user_003', name: '次郎', email: 'jiro@example.com' }
  ];

  return (
    <div className="user-list">
      {users.map(user => (
        <div key={user.id} className="user-item">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  );
}

user.idが一意なので、これをキーとして使用します。

複数の値を組み合わせることもできます:

function CommentList() {
  const comments = [
    { userId: 1, postId: 10, text: 'いい記事ですね!', timestamp: '2024-01-01T10:00:00Z' },
    { userId: 2, postId: 10, text: '参考になりました', timestamp: '2024-01-01T11:00:00Z' }
  ];

  return (
    <div className="comment-list">
      {comments.map(comment => (
        <div key={`${comment.userId}-${comment.postId}-${comment.timestamp}`} className="comment">
          <p>{comment.text}</p>
          <small>ユーザー{comment.userId} - {comment.timestamp}</small>
        </div>
      ))}
    </div>
  );
}

userIdpostIdtimestampを組み合わせて一意のキーを作成しています。

indexをkeyに使っちゃダメな場合

配列の順序が変わる可能性がある場合は、indexを使わない方が安全です。

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '買い物', completed: false },
    { id: 2, text: '掃除', completed: true },
    { id: 3, text: '勉強', completed: false }
  ]);

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div className="todo-list">
      {todos.map(todo => (
        <div key={todo.id} className="todo-item">
          <input 
            type="checkbox" 
            checked={todo.completed}
          />
          <span className={todo.completed ? 'completed' : ''}>{todo.text}</span>
          <button onClick={() => deleteTodo(todo.id)}>削除</button>
        </div>
      ))}
    </div>
  );
}

todo.idを使うことで、削除や並び替えが正しく動作します。

実践的な応用パターン

実際のアプリでよく使われるパターンを学びましょう。

フィルタリング機能付きリスト

import { useState } from 'react';

function TaskManager() {
  const [tasks, setTasks] = useState([
    { id: 1, title: 'プレゼン資料作成', priority: 'high', status: 'pending' },
    { id: 2, title: 'ミーティング参加', priority: 'medium', status: 'completed' },
    { id: 3, title: 'レポート提出', priority: 'high', status: 'pending' },
    { id: 4, title: 'コードレビュー', priority: 'low', status: 'in_progress' }
  ]);

  const [filter, setFilter] = useState('all');

  const filteredTasks = tasks.filter(task => {
    switch (filter) {
      case 'pending':
        return task.status === 'pending';
      case 'completed':
        return task.status === 'completed';
      case 'high_priority':
        return task.priority === 'high';
      default:
        return true;
    }
  });

  const updateTaskStatus = (taskId, newStatus) => {
    setTasks(tasks.map(task => 
      task.id === taskId ? { ...task, status: newStatus } : task
    ));
  };

  return (
    <div className="task-manager">
      <h2>タスク管理</h2>
      
      <div className="controls">
        <select value={filter} onChange={(e) => setFilter(e.target.value)}>
          <option value="all">すべて</option>
          <option value="pending">未着手</option>
          <option value="completed">完了</option>
          <option value="high_priority">高優先度</option>
        </select>
      </div>

      <div className="task-list">
        {filteredTasks.map(task => (
          <div key={task.id} className={`task-item ${task.status}`}>
            <h3 className="task-title">{task.title}</h3>
            <span className="priority-badge">{task.priority}</span>
            
            {task.status === 'pending' && (
              <button onClick={() => updateTaskStatus(task.id, 'in_progress')}>
                開始
              </button>
            )}
            {task.status === 'in_progress' && (
              <button onClick={() => updateTaskStatus(task.id, 'completed')}>
                完了
              </button>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

まずfilter関数でタスクを絞り込みます。 その後map関数でJSX要素に変換。

フィルター→マップの順番が重要です。

updateTaskStatus関数では、mapを使って特定のタスクだけを更新しています。 task.id === taskIdの場合のみ、新しいstatusを設定します。

ネストしたデータの表示

階層構造のデータも扱えます。

function CommentThread() {
  const commentsData = [
    {
      id: 1,
      author: '田中太郎',
      content: 'とても興味深い記事でした!',
      timestamp: '2024-01-10T10:00:00Z',
      replies: [
        {
          id: 11,
          author: '佐藤花子',
          content: '私も同感です。',
          timestamp: '2024-01-10T11:30:00Z',
          replies: [
            {
              id: 111,
              author: '田中太郎',
              content: 'ありがとうございます!',
              timestamp: '2024-01-10T12:00:00Z',
              replies: []
            }
          ]
        }
      ]
    }
  ];

  const CommentItem = ({ comment, level = 0 }) => {
    const [showReplies, setShowReplies] = useState(true);
    const indentStyle = { marginLeft: `${level * 20}px` };

    return (
      <div className="comment-item" style={indentStyle}>
        <div className="comment-content">
          <div className="comment-header">
            <span className="comment-author">{comment.author}</span>
            <span className="comment-timestamp">
              {new Date(comment.timestamp).toLocaleString()}
            </span>
          </div>
          
          <p className="comment-text">{comment.content}</p>
          
          {comment.replies.length > 0 && (
            <button onClick={() => setShowReplies(!showReplies)}>
              {showReplies ? '返信を隠す' : `返信を表示 (${comment.replies.length})`}
            </button>
          )}
        </div>

        {showReplies && comment.replies.length > 0 && (
          <div className="comment-replies">
            {comment.replies.map(reply => (
              <CommentItem 
                key={reply.id} 
                comment={reply} 
                level={level + 1}
              />
            ))}
          </div>
        )}
      </div>
    );
  };

  return (
    <div className="comment-thread">
      <h3>コメント ({commentsData.length})</h3>
      <div className="comments-list">
        {commentsData.map(comment => (
          <CommentItem key={comment.id} comment={comment} />
        ))}
      </div>
    </div>
  );
}

再帰的な構造になっています。 CommentItemコンポーネントが、自分自身を呼び出して返信を表示します。

levelで階層の深さを管理。 marginLeft: ${level * 20}pxで、返信にインデントを付けています。

検索・ソート機能

import { useState, useMemo } from 'react';

function DataTable() {
  const [employees] = useState([
    { id: 1, name: '田中太郎', department: '開発', salary: 800000, age: 32 },
    { id: 2, name: '佐藤花子', department: 'デザイン', salary: 650000, age: 28 },
    { id: 3, name: '山田次郎', department: '営業', salary: 750000, age: 35 }
  ]);

  const [searchTerm, setSearchTerm] = useState('');
  const [sortBy, setSortBy] = useState('name');

  const filteredAndSortedEmployees = useMemo(() => {
    let filtered = employees;

    if (searchTerm) {
      filtered = filtered.filter(emp => 
        emp.name.toLowerCase().includes(searchTerm.toLowerCase())
      );
    }

    filtered.sort((a, b) => {
      if (sortBy === 'salary' || sortBy === 'age') {
        return b[sortBy] - a[sortBy];
      }
      return a[sortBy].localeCompare(b[sortBy]);
    });

    return filtered;
  }, [employees, searchTerm, sortBy]);

  return (
    <div className="data-table">
      <h2>従業員データ</h2>

      <div className="table-controls">
        <input
          type="text"
          placeholder="名前で検索..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="name">名前順</option>
          <option value="salary">給与順</option>
          <option value="age">年齢順</option>
        </select>
      </div>

      <table className="employee-table">
        <thead>
          <tr>
            <th>名前</th>
            <th>部署</th>
            <th>給与</th>
            <th>年齢</th>
          </tr>
        </thead>
        <tbody>
          {filteredAndSortedEmployees.map(employee => (
            <tr key={employee.id}>
              <td>{employee.name}</td>
              <td>{employee.department}</td>
              <td>¥{employee.salary.toLocaleString()}</td>
              <td>{employee.age}歳</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

useMemoを使って、計算結果をメモ化しています。 検索条件やソート条件が変わったときだけ再計算されます。

filterで検索条件に合うデータを抽出。 sortで並び順を調整。

数値(給与、年齢)は数値として比較。 文字列(名前)はlocaleCompareで日本語対応の比較を行います。

パフォーマンスを最適化しよう

大量のデータを扱うときの最適化テクニックです。

React.memoで不要な再レンダリングを防ぐ

import { memo, useState, useCallback } from 'react';

const ListItem = memo(({ item, onUpdate, onDelete }) => {
  console.log(`ListItem rendered: ${item.name}`);

  return (
    <div className="list-item">
      <h3>{item.name}</h3>
      <p>{item.description}</p>
      <p>価格: ¥{item.price.toLocaleString()}</p>
      <div className="item-actions">
        <button onClick={() => onUpdate(item.id)}>編集</button>
        <button onClick={() => onDelete(item.id)}>削除</button>
      </div>
    </div>
  );
});

function OptimizedList() {
  const [items, setItems] = useState([
    { id: 1, name: '商品A', description: '高品質な商品です', price: 1000 },
    { id: 2, name: '商品B', description: '人気の商品です', price: 1500 },
    { id: 3, name: '商品C', description: '新商品です', price: 2000 }
  ]);

  const handleUpdate = useCallback((id) => {
    setItems(prevItems => 
      prevItems.map(item => 
        item.id === id ? { ...item, name: `${item.name} (更新済み)` } : item
      )
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setItems(prevItems => prevItems.filter(item => item.id !== id));
  }, []);

  return (
    <div className="optimized-list">
      <h2>最適化されたリスト</h2>
      <div className="items-grid">
        {items.map(item => (
          <ListItem
            key={item.id}
            item={item}
            onUpdate={handleUpdate}
            onDelete={handleDelete}
          />
        ))}
      </div>
    </div>
  );
}

memoListItemコンポーネントをメモ化。 propsが変わらない限り、再レンダリングされません。

useCallbackでコールバック関数をメモ化。 毎回新しい関数が作られることを防いでいます。

コンソールを見ると、必要なときだけレンダリングされることが確認できます。

無限スクロールで大量データを扱う

import { useState, useEffect, useCallback, useRef } from 'react';

function InfiniteScrollList() {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [page, setPage] = useState(1);
  const observer = useRef();

  const fetchItems = useCallback(async (pageNumber) => {
    setLoading(true);
    try {
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      const newItems = Array.from({ length: 20 }, (_, index) => ({
        id: (pageNumber - 1) * 20 + index + 1,
        title: `記事 ${(pageNumber - 1) * 20 + index + 1}`,
        content: `これは${(pageNumber - 1) * 20 + index + 1}番目の記事の内容です。`,
        author: `著者${Math.floor(Math.random() * 10) + 1}`,
        likes: Math.floor(Math.random() * 100)
      }));

      setItems(prevItems => pageNumber === 1 ? newItems : [...prevItems, ...newItems]);
      setHasMore(pageNumber < 10);
    } catch (error) {
      console.error('データ取得エラー:', error);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchItems(1);
  }, [fetchItems]);

  const lastItemElementRef = useCallback(node => {
    if (loading) return;
    if (observer.current) observer.current.disconnect();
    
    observer.current = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting && hasMore) {
        setPage(prevPage => {
          const nextPage = prevPage + 1;
          fetchItems(nextPage);
          return nextPage;
        });
      }
    });
    
    if (node) observer.current.observe(node);
  }, [loading, hasMore, fetchItems]);

  return (
    <div className="infinite-scroll-list">
      <h2>無限スクロールリスト</h2>
      
      <div className="articles-grid">
        {items.map((item, index) => {
          const isLast = items.length === index + 1;
          const ref = isLast ? lastItemElementRef : null;
          
          return (
            <article key={item.id} ref={ref} className="article-card">
              <h3 className="article-title">{item.title}</h3>
              <p className="article-excerpt">{item.content}</p>
              <div className="article-meta">
                <span>by {item.author}</span>
                <span>❤️ {item.likes}</span>
              </div>
            </article>
          );
        })}
      </div>

      {loading && (
        <div className="loading-indicator">
          <p>読み込み中...</p>
        </div>
      )}

      {!hasMore && (
        <div className="end-message">
          <p>すべての記事を読み込みました</p>
        </div>
      )}
    </div>
  );
}

IntersectionObserverで最後の要素が見えたかどうかを監視。 見えたら次のページのデータを取得します。

fetchItems関数で新しいデータを取得。 setItems(prevItems => [...prevItems, ...newItems])で既存のデータに追加しています。

これで数万件のデータでもスムーズに表示できます。

よくあるエラーと解決方法

Map関数を使っているとよく出会うエラーを見てみましょう。

Key関連の警告

// ❌ 問題のあるコード
function ProblematicList() {
  const items = ['A', 'B', 'C'];
  
  return (
    <ul>
      {items.map(item => (
        <li>{item}</li> // Warning: Each child should have a unique "key" prop
      ))}
    </ul>
  );
}

コンソールに警告が表示されます。

// ✅ 修正されたコード
function FixedList() {
  const items = ['A', 'B', 'C'];
  
  return (
    <ul>
      {items.map((item, index) => (
        <li key={`item-${index}`}>{item}</li>
      ))}
    </ul>
  );
}

keyを追加するだけで解決します。

配列がnullのエラー

// ❌ 問題のあるコード
function ProblematicRender() {
  const users = null; // APIからのデータがまだ取得されていない
  
  return (
    <div>
      {users.map(user => ( // TypeError: Cannot read property 'map' of null
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

usersnullなので、map関数が使えません。

// ✅ 修正されたコード
function FixedRender() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUsers()
      .then(data => {
        setUsers(data || []);
        setLoading(false);
      })
      .catch(err => {
        console.error('エラー:', err);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>読み込み中...</div>;
  
  return (
    <div>
      {users.length === 0 ? (
        <p>ユーザーが見つかりません</p>
      ) : (
        users.map(user => (
          <div key={user.id}>{user.name}</div>
        ))
      )}
    </div>
  );
}

初期値を空配列[]に設定。 データ取得前の状態も適切に処理します。

パフォーマンス問題

// ❌ パフォーマンスが悪いコード
function SlowList({ items }) {
  return (
    <div>
      {items.map(item => (
        <ExpensiveComponent 
          key={item.id} 
          item={item}
          onClick={() => console.log(item.id)} // 毎回新しい関数を作成
          style={{ backgroundColor: item.isActive ? 'green' : 'gray' }} // 毎回新しいオブジェクト
        />
      ))}
    </div>
  );
}

毎回新しい関数やオブジェクトを作成するので、すべてのコンポーネントが再レンダリングされます。

// ✅ 最適化されたコード
function OptimizedList({ items }) {
  const handleClick = useCallback((id) => {
    console.log(id);
  }, []);

  return (
    <div>
      {items.map(item => (
        <MemoizedExpensiveComponent 
          key={item.id} 
          item={item}
          onClick={handleClick}
        />
      ))}
    </div>
  );
}

const MemoizedExpensiveComponent = memo(({ item, onClick }) => {
  const style = useMemo(() => ({
    backgroundColor: item.isActive ? 'green' : 'gray'
  }), [item.isActive]);

  return (
    <div style={style} onClick={() => onClick(item.id)}>
      {item.name}
    </div>
  );
});

useCallbackuseMemomemoを組み合わせて最適化。 不要な再レンダリングを防げます。

まとめ

Reactのmap関数について詳しく解説しました。

基本をしっかり理解しよう

Map関数は配列の各要素を新しい形に変換する関数です。 Reactでは、データ配列をJSX要素配列に変換するために使います。

Key Propsを忘れずに

各要素には一意のkeyを設定しましょう。 できるだけIDを使い、indexは最後の手段です。

実践的なパターンを覚えよう

フィルタリング、ソート、ネストしたデータの表示など、実際のアプリでよく使うパターンを身につけましょう。

パフォーマンスを意識しよう

大量データを扱うときは、memo、useCallback、useMemoを活用しましょう。 無限スクロールなどの最適化手法も有効です。

エラーパターンを知っておこう

よくあるエラーを知っていれば、素早く対処できます。

Map関数はReactの基本中の基本です。

この記事で学んだ内容を実際にコードで試してみてください。 きっと動的で使いやすいWebアプリが作れるようになりますよ!

関連記事