Reactでmapを使った繰り返し処理|基本から実践まで徹底解説
Reactのmap関数を使ったリストレンダリングを基本から応用まで詳しく解説。key props、パフォーマンス最適化、実践的な使用例を紹介
みなさん、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.name
、user.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>
);
}
userId
、postId
、timestamp
を組み合わせて一意のキーを作成しています。
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>
);
}
memo
でListItem
コンポーネントをメモ化。
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>
);
}
users
がnull
なので、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>
);
});
useCallback
、useMemo
、memo
を組み合わせて最適化。
不要な再レンダリングを防げます。
まとめ
Reactのmap関数について詳しく解説しました。
基本をしっかり理解しよう
Map関数は配列の各要素を新しい形に変換する関数です。 Reactでは、データ配列をJSX要素配列に変換するために使います。
Key Propsを忘れずに
各要素には一意のkeyを設定しましょう。 できるだけIDを使い、indexは最後の手段です。
実践的なパターンを覚えよう
フィルタリング、ソート、ネストしたデータの表示など、実際のアプリでよく使うパターンを身につけましょう。
パフォーマンスを意識しよう
大量データを扱うときは、memo、useCallback、useMemoを活用しましょう。 無限スクロールなどの最適化手法も有効です。
エラーパターンを知っておこう
よくあるエラーを知っていれば、素早く対処できます。
Map関数はReactの基本中の基本です。
この記事で学んだ内容を実際にコードで試してみてください。 きっと動的で使いやすいWebアプリが作れるようになりますよ!