React初心者のためのリストレンダリング完全ガイド!配列データを効率的に表示する方法

Reactアプリケーションを開発していると、配列データを画面に一覧表示したい場面が頻繁に発生します。商品カタログ、ユーザー一覧、タスクリストなど、繰り返し表示される要素をどのように効率的に実装するかは、React開発者にとって重要なスキルの一つです。

この記事では、Reactでリストを表示する方法を基礎から応用まで、初学者にも分かりやすく解説します。JavaScriptの配列メソッドを活用しながら、実用的なサンプルコードとともに学んでいきましょう。

目次

リストレンダリングとは何か

リストレンダリングとは、配列データを元にして、同じ構造のコンポーネントを複数表示する技術のことです。例えば、ECサイトの商品一覧や、SNSの投稿リストなど、類似したレイアウトの要素を大量に表示する際に使用します。

従来のHTML書き方との比較

従来のHTMLでリストを作成する場合、以下のように手動でタグを記述する必要がありました。

<ul>
  <li>商品A - 1000円</li>
  <li>商品B - 2000円</li>
  <li>商品C - 1500円</li>
</ul>

しかし、Reactでは配列データを動的に変換して表示できるため、より柔軟で保守性の高いコードを書くことができます。

基本的な配列からコンポーネントへの変換

まずは、最もシンプルな例から見ていきましょう。商品名の配列をリスト表示する基本的なパターンです。

データ配列の準備

const products = [
  '有機野菜セット',
  '国産牛ステーキ',
  '手作りパン',
  '新鮮な魚介類'
];

map()メソッドを使った変換

map() は JavaScript の配列メソッドで、配列の各要素を変換して新しい配列を作成します。Reactでは、データの配列をJSX要素の配列に変換するためによく使われます。

function ProductList() {
  const products = [
    '有機野菜セット',
    '国産牛ステーキ',
    '手作りパン',
    '新鮮な魚介類'
  ];

  const productItems = products.map((product, index) => (
    <li key={index}>{product}</li>
  ));

  return (
    <div>
      <h2>おすすめ商品一覧</h2>
      <ul>{productItems}</ul>
    </div>
  );
}

より実用的なオブジェクト配列の活用

実際の開発では、文字列の配列よりもオブジェクトの配列を扱うことが多くなります。以下のような構造化されたデータを使用してみましょう。

function EnhancedProductList() {
  const products = [
    { id: 1, name: '有機野菜セット', price: 2800, category: '野菜' },
    { id: 2, name: '国産牛ステーキ', price: 4500, category: '肉類' },
    { id: 3, name: '手作りパン', price: 650, category: 'パン' },
    { id: 4, name: '新鮮な魚介類', price: 3200, category: '魚介' }
  ];

  return (
    <div>
      <h2>商品カタログ</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <strong>{product.name}</strong> - 
            ¥{product.price.toLocaleString()} 
            ({product.category})
          </li>
        ))}
      </ul>
    </div>
  );
}

filter()メソッドを使った絞り込み表示

すべてのアイテムを表示するだけでなく、特定の条件に合致するものだけを表示したい場合があります。そのような場合にはfilter()メソッドを活用します。

価格による絞り込みの実装

function FilteredProductList() {
  const products = [
    { id: 1, name: '有機野菜セット', price: 2800, category: '野菜', inStock: true },
    { id: 2, name: '国産牛ステーキ', price: 4500, category: '肉類', inStock: false },
    { id: 3, name: '手作りパン', price: 650, category: 'パン', inStock: true },
    { id: 4, name: '新鮮な魚介類', price: 3200, category: '魚介', inStock: true },
    { id: 5, name: '特選フルーツ', price: 1800, category: 'フルーツ', inStock: false }
  ];

  // 在庫があり、価格が3000円以下の商品を絞り込み
  const affordableInStockProducts = products.filter(product => 
    product.inStock && product.price <= 3000
  );

  return (
    <div>
      <h2>お手頃価格の在庫商品</h2>
      <p>3000円以下で在庫のある商品をご紹介します</p>
      <ul>
        {affordableInStockProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price.toLocaleString()}
            <span style={{color: 'green', marginLeft: '10px'}}>
              ✓ 在庫あり
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

複数条件による絞り込み

function AdvancedFilterExample() {
  const products = [
    { id: 1, name: '有機野菜セット', price: 2800, category: '野菜', rating: 4.8 },
    { id: 2, name: '国産牛ステーキ', price: 4500, category: '肉類', rating: 4.9 },
    { id: 3, name: '手作りパン', price: 650, category: 'パン', rating: 4.2 },
    { id: 4, name: '新鮮な魚介類', price: 3200, category: '魚介', rating: 4.6 }
  ];

  // 高評価(4.5以上)の商品のみを表示
  const highRatedProducts = products.filter(product => product.rating >= 4.5);
  
  // カテゴリー別のグループ化も同時に実現
  const premiumVegetables = products.filter(product => 
    product.category === '野菜' && product.rating >= 4.5
  );

  return (
    <div>
      <h2>プレミアム商品セレクション</h2>
      <h3>高評価商品(4.5★以上)</h3>
      <ul>
        {highRatedProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price.toLocaleString()}
            <span style={{color: 'gold', marginLeft: '10px'}}>
              ★{product.rating}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

keyプロパティの重要性と正しい使い方

Reactでリストをレンダリングする際に最も重要な概念の一つがkeyプロパティです。keyはReactがリストの各要素を識別し、効率的にDOMを更新するために使用されます。

なぜkeyが必要なのか

Reactは仮想DOMを使用してパフォーマンスを最適化していますが、リストの要素が変更された際にどの要素が追加・削除・移動されたかを正確に把握するためにkeyが必要になります。

よくある間違い
配列のインデックスをkeyとして使用するのは避けましょう。リストの順序が変更される可能性がある場合、予期しない動作を引き起こす可能性があります。

正しいkeyの選び方

function TaskList() {
  const tasks = [
    { id: 'task-1', title: 'React学習', completed: false, priority: 'high' },
    { id: 'task-2', title: '買い物', completed: true, priority: 'medium' },
    { id: 'task-3', title: '運動', completed: false, priority: 'low' }
  ];

  return (
    <div>
      <h2>今日のタスク</h2>
      <ul>
        {tasks.map(task => (
          // 一意のIDをkeyとして使用(推奨)
          <li key={task.id} style={{
            textDecoration: task.completed ? 'line-through' : 'none',
            color: task.priority === 'high' ? 'red' : 'black'
          }}>
            {task.title} 
            {task.completed && <span> ✓</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

keyが不適切な場合の問題例

❌ 悪い例:インデックスをkeyに使用

function BadKeyExample() {
  const [items, setItems] = useState(['りんご', 'バナナ', 'オレンジ']);

  const addItem = () => {
    setItems(['新しいフルーツ', ...items]); // 先頭に追加
  };

  return (
    <div>
      <button onClick={addItem}>アイテム追加</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li> {/* インデックスを使用は危険 */}
        ))}
      </ul>
    </div>
  );
}

✅ 良い例:一意のIDをkeyに使用

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

  const addItem = () => {
    const newItem = { id: Date.now(), name: '新しいフルーツ' };
    setItems([newItem, ...items]);
  };

  return (
    <div>
      <button onClick={addItem}>アイテム追加</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name}</li> {/* 一意のIDを使用 */}
        ))}
      </ul>
    </div>
  );
}

実践的なサンプル:タスク管理アプリケーション

これまで学んだ概念を組み合わせて、実用的なタスク管理アプリケーションのコンポーネントを作成してみましょう。

function TaskManager() {
  const tasks = [
    { 
      id: 'tsk_001', 
      title: 'Reactのリストレンダリングを学習する', 
      completed: false, 
      priority: 'high',
      dueDate: '2024-01-15',
      category: '学習'
    },
    { 
      id: 'tsk_002', 
      title: '食材の買い出しに行く', 
      completed: true, 
      priority: 'medium',
      dueDate: '2024-01-10',
      category: '生活'
    },
    { 
      id: 'tsk_003', 
      title: 'プロジェクトの企画書を作成', 
      completed: false, 
      priority: 'high',
      dueDate: '2024-01-20',
      category: '仕事'
    },
    { 
      id: 'tsk_004', 
      title: 'ジムでのトレーニング', 
      completed: false, 
      priority: 'low',
      dueDate: '2024-01-12',
      category: '健康'
    }
  ];

  // 優先度による絞り込み
  const highPriorityTasks = tasks.filter(task => 
    task.priority === 'high' && !task.completed
  );

  // 完了済みタスク
  const completedTasks = tasks.filter(task => task.completed);

  // カテゴリー別のタスク数を計算
  const categoryCount = tasks.reduce((acc, task) => {
    acc[task.category] = (acc[task.category] || 0) + 1;
    return acc;
  }, {});

  const getPriorityStyle = (priority) => {
    const styles = {
      high: { color: '#e74c3c', fontWeight: 'bold' },
      medium: { color: '#f39c12' },
      low: { color: '#27ae60' }
    };
    return styles[priority] || {};
  };

  return (
    <div>
      <h2>📋 タスク管理ダッシュボード</h2>
      
      {/* カテゴリー統計 */}
      <div style={{ marginBottom: '20px', padding: '10px', backgroundColor: '#f8f9fa' }}>
        <h3>カテゴリー別タスク数</h3>
        <ul>
          {Object.entries(categoryCount).map(([category, count]) => (
            <li key={category}>
              {category}: {count}件
            </li>
          ))}
        </ul>
      </div>

      {/* 高優先度タスク */}
      <div style={{ marginBottom: '30px' }}>
        <h3 style={{ color: '#e74c3c' }}>🔥 緊急タスク</h3>
        {highPriorityTasks.length > 0 ? (
          <ul>
            {highPriorityTasks.map(task => (
              <li key={task.id} style={{ marginBottom: '8px' }}>
                <strong>{task.title}</strong>
                <div style={{ fontSize: '0.9em', color: '#666' }}>
                  期限: {task.dueDate} | カテゴリー: {task.category}
                </div>
              </li>
            ))}
          </ul>
        ) : (
          <p style={{ color: '#27ae60' }}>緊急のタスクはありません!</p>
        )}
      </div>

      {/* 全タスク一覧 */}
      <div style={{ marginBottom: '30px' }}>
        <h3>📝 全タスク一覧</h3>
        <ul>
          {tasks.map(task => (
            <li key={task.id} style={{
              marginBottom: '12px',
              padding: '10px',
              backgroundColor: task.completed ? '#d5f4e6' : '#fff',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}>
              <div style={{
                textDecoration: task.completed ? 'line-through' : 'none'
              }}>
                <strong>{task.title}</strong>
                {task.completed && <span style={{ color: '#27ae60' }}> ✅</span>}
              </div>
              <div style={{ fontSize: '0.9em', marginTop: '5px' }}>
                <span style={getPriorityStyle(task.priority)}>
                  優先度: {task.priority}
                </span>
                <span style={{ marginLeft: '15px' }}>
                  期限: {task.dueDate}
                </span>
                <span style={{ marginLeft: '15px' }}>
                  カテゴリー: {task.category}
                </span>
              </div>
            </li>
          ))}
        </ul>
      </div>

      {/* 完了済みタスク */}
      <div>
        <h3 style={{ color: '#27ae60' }}>✅ 完了済みタスク ({completedTasks.length}件)</h3>
        {completedTasks.length > 0 ? (
          <ul>
            {completedTasks.map(task => (
              <li key={task.id} style={{ 
                color: '#666', 
                textDecoration: 'line-through',
                marginBottom: '5px'
              }}>
                {task.title} - {task.category}
              </li>
            ))}
          </ul>
        ) : (
          <p>完了済みのタスクはまだありません。</p>
        )}
      </div>
    </div>
  );
}

パフォーマンス最適化のポイント

大量のデータを扱う際は、パフォーマンスについても考慮する必要があります。

React.memoを活用したコンポーネント最適化

React.memo は高次コンポーネント(HOC)で、コンポーネントの不要な再レンダリングを防ぐための最適化手法です。

基本的な仕組み:

  • コンポーネントのpropsが前回と同じ場合、再レンダリングをスキップ
  • メモ化(memoization)によってパフォーマンスを向上

使用前後の比較

React.memo を使わない場合
function TaskItem({ task, onToggle }) {
  console.log(`TaskItem ${task.id} がレンダリングされました`);
  return (
    <li>
      <input 
        type="checkbox" 
        checked={task.completed}
        onChange={() => onToggle(task.id)}
      />
      <span>{task.title}</span>
    </li>
  );
}

// 親コンポーネントが再レンダリングされると、
// すべてのTaskItemも再レンダリングされる
React.memo を使った場合
const TaskItem = React.memo(function TaskItem({ task, onToggle }) {
  console.log(`TaskItem ${task.id} がレンダリングされました`);
  return (
    <li style={{
      textDecoration: task.completed ? 'line-through' : 'none',
      marginBottom: '8px'
    }}>
      <input 
        type="checkbox" 
        checked={task.completed}
        onChange={() => onToggle(task.id)}
      />
      <span style={{ marginLeft: '8px' }}>{task.title}</span>
    </li>
  );
});

// propsが変わらない限り、再レンダリングされない

実際の動作例

function App() {
  const [tasks, setTasks] = useState([
    { id: 1, title: 'タスク1', completed: false },
    { id: 2, title: 'タスク2', completed: false },
    { id: 3, title: 'タスク3', completed: false }
  ]);

  const handleTaskToggle = useCallback((taskId) => {
    setTasks(prevTasks => 
      prevTasks.map(task => 
        task.id === taskId 
          ? { ...task, completed: !task.completed }
          : task
      )
    );
  }, []);

  return (
    <div>
      <h1>タスクリスト</h1>
      <OptimizedTaskList tasks={tasks} onTaskToggle={handleTaskToggle} />
    </div>
  );
}

動作の流れ:

  1. タスク1のチェックボックスをクリック
  2. tasks配列が更新される
  3. タスク1のTaskItemのみ再レンダリング
  4. タスク2、3は props が変わらないので再レンダリングされない

React.memo の比較ルール

デフォルトの比較(浅い比較)
const TaskItem = React.memo(function TaskItem({ task, onToggle }) {
  // task オブジェクトの参照が変わった場合のみ再レンダリング
  // task.title や task.completed の値が同じでも、
  // オブジェクト自体が新しく作られていれば再レンダリングされる
});

カスタム比較関数

const TaskItem = React.memo(function TaskItem({ task, onToggle }) {
  return (
    <li>
      <input 
        type="checkbox" 
        checked={task.completed}
        onChange={() => onToggle(task.id)}
      />
      <span>{task.title}</span>
    </li>
  );
}, (prevProps, nextProps) => {
  // true を返すと再レンダリングをスキップ
  // false を返すと再レンダリングを実行
  return (
    prevProps.task.id === nextProps.task.id &&
    prevProps.task.title === nextProps.task.title &&
    prevProps.task.completed === nextProps.task.completed &&
    prevProps.onToggle === nextProps.onToggle
  );
});

useCallback との組み合わせ

関数propsは毎回新しい参照になりがちなので、useCallbackと組み合わせることが重要です。

function ParentComponent() {
  const [tasks, setTasks] = useState([]);

  // ❌ 悪い例:毎回新しい関数が作られる
  const handleToggle = (taskId) => {
    // 処理
  };

  // ✅ 良い例:useCallbackでメモ化
  const handleToggle = useCallback((taskId) => {
    setTasks(prevTasks => 
      prevTasks.map(task => 
        task.id === taskId 
          ? { ...task, completed: !task.completed }
          : task
      )
    );
  }, []); // 依存配列が空なので、関数は一度だけ作られる

  return (
    <OptimizedTaskList tasks={tasks} onTaskToggle={handleToggle} />
  );
}

使用する場面

React.memo が有効な場面
  • リストアイテム(今回の例のように)
  • 重い計算処理を含むコンポーネント
  • 頻繁に親が再レンダリングされるが、子のpropsは変わらない場合
  • 多数の子コンポーネントがある場合
使わない方が良い場面
  • propsが頻繁に変わるコンポーネント
  • 軽量な処理しかしないコンポーネント
  • 比較処理のコストがレンダリングコストを上回る場合

実践的な最適化例

// 商品リストの最適化例
const ProductCard = React.memo(function ProductCard({ 
  product, 
  onAddToCart, 
  onToggleFavorite 
}) {
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>¥{product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>
        カートに追加
      </button>
      <button onClick={() => onToggleFavorite(product.id)}>
        {product.isFavorite ? '❤️' : '🤍'}
      </button>
    </div>
  );
});

function ProductList({ products, onAddToCart, onToggleFavorite }) {
  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
          onToggleFavorite={onToggleFavorite}
        />
      ))}
    </div>
  );
}

ヒント
数千件を超える大量のリストを表示する場合は、react-windowreact-virtualized といったライブラリを使用した仮想スクロールの実装を検討しましょう。

よくある間違いとその対処法

keyプロパティの不適切な使用

// ❌ 間違った例
{items.map((item, index) => (
  <div key={index}>{item.name}</div>
))}

// ❌ さらに悪い例
{items.map(item => (
  <div key={Math.random()}>{item.name}</div>
))}

// ✅ 正しい例
{items.map(item => (
  <div key={item.id}>{item.name}</div>
))}

直接的な配列の変更

// ❌ 間違った例:元配列を直接変更
const sortedTasks = tasks.sort((a, b) => a.priority.localeCompare(b.priority));

// ✅ 正しい例:新しい配列を作成
const sortedTasks = [...tasks].sort((a, b) => a.priority.localeCompare(b.priority));

map内でのPromiseの不適切な使用

// ❌ 間違った例:元配列を直接変更
const sortedTasks = tasks.sort((a, b) => a.priority.localeCompare(b.priority));

// ✅ 正しい例:新しい配列を作成
const sortedTasks = [...tasks].sort((a, b) => a.priority.localeCompare(b.priority));

まとめ

Reactにおけるリストレンダリングは、現代のWebアプリケーション開発において欠かせない技術です。
この記事で学んだポイントです。

  • map()メソッドを使用して配列データをJSXコンポーネントに変換する
  • filter()メソッドで条件に合致するデータのみを表示する
  • keyプロパティには一意で安定した値を使用する
  • パフォーマンスを考慮した実装を心がける
  • よくある間違いを理解し、適切な解決策を適用する

これらの概念をマスターすることで、動的で効率的なReactアプリケーションを構築できるようになります。まずは簡単な例から始めて、徐々に複雑な要件に対応できるよう練習を重ねていきましょう。

実際のプロジェクトでは、ユーザビリティやアクセシビリティも考慮しながら、適切なリストレンダリングの実装を選択することが重要です。今回学んだ基礎知識を活かして、ユーザーフレンドリーなアプリケーションを開発してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次