Chapter 06

リストと条件分岐

配列データをリスト表示する方法と、条件に応じてUIを切り替える方法を学びます。現実のアプリに欠かせないパターンです。

50 min

このチャプターで学ぶこと

  • .map()を使って配列からJSXのリストを生成できる
  • keyプロップの役割を理解して適切に設定できる
  • &&演算子、三項演算子、アーリーリターンで条件分岐ができる
  • リストと条件分岐を組み合わせてUIを作れる
  • シンプルなTodoリスト(追加・完了切り替え)を自力で作れる

リストと条件分岐

データベースから取得したユーザー一覧を表示したい、ステータスによって表示を変えたい、そんな場面は毎日あります。このチャプターではReactでのリスト表示と条件分岐を学びましょう。

配列を .map() でレンダリングする

PHPのforeachに相当するのが、JavaScriptの .map() です。

<!-- Blade (PHP) での書き方 -->
@foreach($users as $user)
  <li>{{ $user->name }}</li>
@endforeach
// React (JSX) での書き方
const users = ['田中', '鈴木', '佐藤'];

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

.map() は配列の各要素を変換して、新しい配列を返します。ここではそれぞれの名前を <li> 要素に変換しています。

📝 .map() の基本

.map() はJavaScriptの配列メソッドです。各要素を受け取り、返した値で新しい配列を作ります。

const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
// doubled は [2, 4, 6]

ReactではJSX要素の配列を返すために使います。

オブジェクトの配列を扱う

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

import { useState } from 'react';

// ユーザーの配列(APIから取得した想定)
const users = [
  { id: 1, name: '田中太郎', email: 'tanaka@example.com', role: '管理者' },
  { id: 2, name: '鈴木花子', email: 'suzuki@example.com', role: '一般' },
  { id: 3, name: '佐藤次郎', email: 'sato@example.com', role: '一般' },
];

function UserList() {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <strong>{user.name}</strong>
          <span> ({user.role})</span>
          <br />
          <small>{user.email}</small>
        </li>
      ))}
    </ul>
  );
}

key プロップ:なぜ必要か

上のコードを見ると key={user.id} が付いています。これは必須です。付けないとコンソールに警告が出ます。

Warning: Each child in a list should have a unique "key" prop.

key が必要な理由

Reactはリストの中のどの要素が変更・追加・削除されたかを効率よく把握するために key を使います。

例えば、リストに新しいアイテムが追加されたとき:

変更前: [A, B, C]
変更後: [A, B, C, D]  ← D が末尾に追加された

key があれば「Aは変わってない、Bも、Cも、Dが新しい!」と素早く判断できます。

key がないと、Reactはリスト全体を再描画しようとして、パフォーマンスが低下します。また、inputのフォーカスやアニメーション状態が予期せずリセットされることもあります。

key には何を使うべきか

{/* 良い例: DBのIDなど一意の値 */}
{items.map((item) => (
  <Item key={item.id} {...item} />
))}

{/* 良い例: 一意の文字列 */}
{tags.map((tag) => (
  <Tag key={tag.slug} name={tag.name} />
))}

{/* 悪い例: インデックスは避けるべき(順序が変わると問題が起きる) */}
{items.map((item, index) => (
  <Item key={index} {...item} />  // リストの順序が変わらない場合のみ許容
))}
⚠️ 配列のインデックスをkeyに使わないほうがいい理由

例えば [A, B, C] の配列があり、A を削除すると [B, C] になります。

  • インデックスをkeyにすると: B が key=0、C が key=1 になる(変更前はB=1、C=2だった)
  • ReactはBとCが別物に変わったと誤解して余分な再描画が起きる

DBから取得したデータなら id を使えばほぼ問題ありません。

key はコンポーネントの外側の要素に付ける

// OK: .map() が返す一番外側の要素に key を付ける
{items.map((item) => (
  <div key={item.id}>
    <h3>{item.title}</h3>
    <p>{item.body}</p>
  </div>
))}

// NG: 内側の要素に付けても意味がない
{items.map((item) => (
  <div>
    <h3 key={item.id}>{item.title}</h3>  {/* これは間違い */}
    <p>{item.body}</p>
  </div>
))}

条件分岐: && 演算子

「ある条件のときだけ表示する」ときによく使います。

function Notification({ count }) {
  return (
    <div>
      <h1>ダッシュボード</h1>
      {/* count が 0 より大きいときだけ表示 */}
      {count > 0 && (
        <div className="badge">
          {count}件の通知があります
        </div>
      )}
    </div>
  );
}

A && B は「A が truthy なら B を返す、falsy なら A を返す」という JavaScript の動作を利用しています。

⚠️ 0 のときに注意

count && <p>{count}件</p> と書くと、count が 0 のとき画面に 0 と表示されてしまいます。

// 危険: count が 0 だと "0" が表示される
{count && <Badge count={count} />}

// 安全: 明示的に真偽値に変換する
{count > 0 && <Badge count={count} />}
{!!count && <Badge count={count} />}

条件分岐: 三項演算子

「条件によってAかBを表示する」ときに使います。

function StatusBadge({ isActive }) {
  return (
    <span style={{ color: isActive ? 'green' : 'gray' }}>
      {isActive ? '稼働中' : '停止中'}
    </span>
  );
}
import { useState } from 'react';

function LoginStatus() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div>
      {isLoggedIn ? (
        // ログイン中の表示
        <div>
          <p>ようこそ!</p>
          <button onClick={() => setIsLoggedIn(false)}>ログアウト</button>
        </div>
      ) : (
        // 未ログインの表示
        <div>
          <p>ログインしてください</p>
          <button onClick={() => setIsLoggedIn(true)}>ログイン</button>
        </div>
      )}
    </div>
  );
}

条件分岐: アーリーリターン

コンポーネント自体の表示・非表示を制御したいときや、ローディング状態を処理するときはアーリーリターンが便利です。

function UserProfile({ user, isLoading }) {
  // ローディング中はスピナーを返す
  if (isLoading) {
    return <p>読み込み中...</p>;
  }

  // ユーザーデータがない場合
  if (!user) {
    return <p>ユーザーが見つかりません</p>;
  }

  // 正常な場合の表示
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
💡 どの条件分岐を使うべきか
  • 単純な表示・非表示&& 演算子
  • AかBかを選ぶ → 三項演算子
  • 複数の状態を処理する(ローディング、エラー、正常など) → アーリーリターン

三項演算子はネストすると読みにくくなるため、3パターン以上になる場合はアーリーリターンか、別の変数に条件分岐の結果を入れる方法を使いましょう。

リストと条件分岐の組み合わせ

実際のアプリでは、リストの中で条件分岐することがよくあります。

import { useState } from 'react';

const products = [
  { id: 1, name: 'Tシャツ', price: 2000, inStock: true },
  { id: 2, name: 'パーカー', price: 5000, inStock: false },
  { id: 3, name: 'ジーンズ', price: 8000, inStock: true },
  { id: 4, name: 'キャップ', price: 3000, inStock: false },
];

function ProductList() {
  const [showInStockOnly, setShowInStockOnly] = useState(false);

  // フィルタリング
  const displayProducts = showInStockOnly
    ? products.filter((p) => p.inStock)
    : products;

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={showInStockOnly}
          onChange={(e) => setShowInStockOnly(e.target.checked)}
        />
        在庫ありのみ表示
      </label>

      {/* 商品がない場合のメッセージ */}
      {displayProducts.length === 0 && (
        <p>該当する商品がありません</p>
      )}

      <ul>
        {displayProducts.map((product) => (
          <li key={product.id}>
            <strong>{product.name}</strong>
            <span> ¥{product.price.toLocaleString()}</span>
            {/* 在庫切れの場合はバッジを表示 */}
            {!product.inStock && (
              <span style={{ color: 'red', marginLeft: '8px' }}>在庫切れ</span>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}
✍ やってみよう

演習: シンプルなTodoリストを作ろう

アイテムの追加と完了チェックができるTodoリストを作りましょう。

localhost:5173
Todoリストのデモ
テキストを入力して追加、チェックで完了状態を切り替え

要件:

  • テキストフィールドと「追加」ボタンでTodoを追加できる
  • 各Todoにはチェックボックスがあり、クリックで完了・未完了を切り替えられる
  • 完了したTodoは打ち消し線で表示する
  • 入力フィールドが空のときは追加できない

データの形:

// Todo アイテムのデータ構造
const [todos, setTodos] = useState([
  { id: 1, text: 'Reactを学ぶ', completed: false },
  { id: 2, text: 'Todoリストを作る', completed: false },
]);

ヒント:

import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Reactを学ぶ', completed: false },
  ]);
  const [inputText, setInputText] = useState('');

  function handleAdd() {
    if (!inputText.trim()) return; // 空のときは追加しない

    const newTodo = {
      id: Date.now(), // 簡易的なユニークID
      text: inputText,
      completed: false,
    };

    // 既存のTodoに新しいものを追加した新しい配列を作る
    setTodos([...todos, newTodo]);
    setInputText(''); // 入力欄をリセット
  }

  function handleToggle(id) {
    // 対象のTodoのcompletedだけ反転させた新しい配列を作る
    setTodos(todos.map((todo) =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  }

  return (
    <div>
      <div>
        <input
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          placeholder="新しいタスク"
        />
        <button onClick={handleAdd}>追加</button>
      </div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggle(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

チャレンジ:

  • 「削除」ボタンを各Todoに追加しよう(filter() を使う)
  • 完了したTodoの数と全体のTodo数を表示しよう(例:「3/5 完了」)
  • Enter キーでも追加できるようにしよう(onKeyDown を使う)
📝 Stateの更新と配列

Reactでは Stateを直接変更してはいけないルールがあります。配列を操作するときは、元の配列を変えずに新しい配列を作るのがポイントです。

// NG: 元の配列を直接変更している
todos.push(newTodo);
setTodos(todos); // これは再レンダリングされない!

// OK: スプレッド構文で新しい配列を作る
setTodos([...todos, newTodo]);

// 削除も同様
setTodos(todos.filter((todo) => todo.id !== targetId));

まとめ

次のチャプターでは useEffect を学びます。APIからデータを取得したり、タイマーを設定したりと、コンポーネントの「外の世界」と連携する方法を学びましょう。