条件付きレンダリングとリスト

React では、条件に応じて異なる UI を表示したり、配列データからリストを動的に描画したりすることが頻繁にあります。 この章では、条件付きレンダリングの複数のパターンと、リスト描画における key の重要性について解説します。

条件付きレンダリングとは

条件付きレンダリング(Conditional Rendering)とは、 ある条件に基づいて表示する UI を切り替える手法です。 例えば、ログイン状態によって表示するコンポーネントを変えたり、 データの読み込み中にローディング表示を出したりする場面で使います。

React では、JavaScript の制御構文(if、三項演算子、&&)をそのまま JSX 内で使うことができます。 特別なテンプレート構文は不要で、すべて純粋な JavaScript で表現します。

if 文による条件分岐

最もシンプルな条件付きレンダリングは、if 文を使った早期リターンパターンです。 条件を満たさない場合に別の JSX を返すか、null を返して何も表示しないようにします。

早期リターンパターン
function Dashboard({ isLoggedIn }) {
  if (!isLoggedIn) {
    return <p>ログインしてください。</p>;
  }

  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ようこそ!こちらがダッシュボードです。</p>
    </div>
  );
}

ローディング表示の例

Loading.jsx
function UserProfile({ isLoading, error, user }) {
  if (isLoading) {
    return <p>読み込み中...</p>;
  }

  if (error) {
    return <p className="error">エラー: {error.message}</p>;
  }

  if (!user) {
    return <p>ユーザーが見つかりません。</p>;
  }

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

三項演算子

JSX の中で条件分岐を行う場合、三項演算子(条件 ? A : B)が最もよく使われます。 if 文は JSX 式の内部に直接書けませんが、三項演算子は式なので埋め込めます。

三項演算子の例
function Greeting({ isLoggedIn, userName }) {
  return (
    <header>
      {isLoggedIn
        ? <p>こんにちは、{userName} さん!</p>
        : <p>ゲストとしてアクセスしています。</p>
      }
    </header>
  );
}

ネストした三項演算子(非推奨)

三項演算子をネスト(入れ子)にすると可読性が大きく低下します。 条件が複雑な場合は、早期リターンパターンや変数に分離する方法を使いましょう。

NG: ネストした三項演算子
// 読みにくい! 避けるべきパターン
return (
  <div>
    {isLoading
      ? <p>読み込み中...</p>
      : error
        ? <p>エラーが発生しました</p>
        : <p>{data}</p>
    }
  </div>
);
OK: 変数に分離
function DataDisplay({ isLoading, error, data }) {
  let content;

  if (isLoading) {
    content = <p>読み込み中...</p>;
  } else if (error) {
    content = <p>エラーが発生しました</p>;
  } else {
    content = <p>{data}</p>;
  }

  return <div>{content}</div>;
}

&& 演算子

「条件が true のときだけ表示する」というシンプルなケースでは、 論理 AND 演算子(&&が便利です。 左辺が true のときだけ右辺が評価(表示)されます。

&& 演算子の例
function Notification({ messages }) {
  return (
    <div>
      <h2>通知</h2>
      {messages.length > 0 && (
        <p>{messages.length} 件の未読メッセージがあります。</p>
      )}
    </div>
  );
}

&& 演算子の落とし穴: 左辺に 0 を使うと、false ではなく 0 がそのまま画面に表示されてしまいます。 これは JavaScript の短絡評価で 0 が falsy かつ数値として返されるためです。 数値を条件にする場合は count > 0 && ... のように比較演算子を使うか、 !!count && ... のように明示的にブール値に変換しましょう。

0 の落とし穴
const count = 0;

// NG: 画面に "0" が表示されてしまう
{count && <p>{count}</p>}

// OK: 比較演算子を使う
{count > 0 && <p>{count}</p>}

条件付きレンダリングのパターン比較

パターン 適した場面 特徴
if + 早期リターン コンポーネント全体の切り替え 読みやすい。複数条件に向く。
三項演算子 ? : JSX 内で A or B を表示 JSX 埋め込み可。ネストは避ける。
&& 条件が true のときだけ表示 短く書ける。0 の落とし穴に注意。
変数に代入 条件が複雑な場合 可読性が高い。if/else を使える。

リストの描画

配列のデータから複数の要素を描画するには、JavaScript の map() メソッドを使います。 map() は配列の各要素に対して関数を実行し、その戻り値で新しい配列を作成します。

FruitList.jsx
function FruitList() {
  const fruits = ['りんご', 'バナナ', 'みかん', 'ぶどう'];

  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>
      ))}
    </ul>
  );
}

オブジェクトの配列を描画

実際のアプリケーションでは、オブジェクトの配列を描画することがほとんどです。

ProductList.jsx
function ProductList() {
  const products = [
    { id: 1, name: 'ノートPC',    price: 89800 },
    { id: 2, name: 'マウス',      price: 3200  },
    { id: 3, name: 'キーボード',  price: 12500 },
    { id: 4, name: 'モニター',    price: 45000 },
  ];

  return (
    <table>
      <thead>
        <tr>
          <th>商品名</th>
          <th>価格</th>
        </tr>
      </thead>
      <tbody>
        {products.map(product => (
          <tr key={product.id}>
            <td>{product.name}</td>
            <td>{product.price.toLocaleString()}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default ProductList;

key の重要性

map() でリストを描画するとき、各要素には一意の key 属性を必ず指定します。 key は React が各要素を識別し、効率的に差分更新を行うために使われます。

key が必要な理由

React はリストの更新時、key を手がかりにして「どの要素が追加・削除・並び替えされたか」を判断します。 key がなければ、React はリスト全体を再描画する必要があり、パフォーマンスが低下します。 さらに、入力フィールドなど内部状態を持つ要素では、State が別の要素に紐付いてしまうバグが発生する可能性があります。

key のルール

key の正しい使い方
// OK: データの一意な ID を使う(推奨)
{users.map(user => (
  <UserCard key={user.id} name={user.name} />
))}

// OK: ユニークな文字列を使う
{countries.map(country => (
  <li key={country.code}>{country.name}</li>
))}

// NG: Math.random() はレンダーごとに変わるため使わない
{items.map(item => (
  <li key={Math.random()}>{item}</li>
))}

key のアンチパターン

配列の index を key に使うのは避けてください。 リストの並び替え、追加、削除が行われると、index と要素の対応が崩れ、 React が要素を正しく識別できなくなります。 その結果、入力フィールドの値が別の行に紐付いたり、 アニメーションが正しく動作しなかったりするバグが発生します。

index を key にしても問題ないケースは、以下のすべてを満たす場合に限られます。

index を key にした場合の問題
import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '買い物' },
    { id: 2, text: '掃除' },
    { id: 3, text: '料理' },
  ]);

  function handleRemoveFirst() {
    setTodos(todos.slice(1));  // 先頭を削除
  }

  return (
    <div>
      <button onClick={handleRemoveFirst}>先頭を削除</button>
      <ul>
        {todos.map((todo, index) => (
          {/* NG: index を key にすると、先頭削除時に表示がずれる */}
          <li key={index}>
            <input type="text" defaultValue={todo.text} />
          </li>
        ))}
      </ul>

      <h3>正しい key を使った場合:</h3>
      <ul>
        {todos.map(todo => (
          {/* OK: 一意の ID を使う */}
          <li key={todo.id}>
            <input type="text" defaultValue={todo.text} />
          </li>
        ))}
      </ul>
    </div>
  );
}

実践例:フィルタリングとソート

実際のアプリケーションでは、リストの表示にフィルタリング(絞り込み)やソート(並び替え)を組み合わせることが多いです。 以下の例では、商品リストを「カテゴリでフィルタ」「価格でソート」する機能を実装しています。

FilterableProductList.jsx
import { useState } from 'react';

const PRODUCTS = [
  { id: 1, name: 'ノートPC',    category: 'PC',   price: 89800  },
  { id: 2, name: 'マウス',      category: '周辺機器', price: 3200 },
  { id: 3, name: 'キーボード',  category: '周辺機器', price: 12500 },
  { id: 4, name: 'デスクトップPC', category: 'PC',  price: 128000 },
  { id: 5, name: 'モニター',    category: '周辺機器', price: 45000 },
  { id: 6, name: 'タブレット',  category: 'PC',   price: 54800  },
];

function FilterableProductList() {
  const [category, setCategory] = useState('all');
  const [sortOrder, setSortOrder] = useState('asc');

  // 1. フィルタリング
  const filtered = category === 'all'
    ? PRODUCTS
    : PRODUCTS.filter(p => p.category === category);

  // 2. ソート(元の配列を変更しないよう [...] でコピー)
  const sorted = [...filtered].sort((a, b) =>
    sortOrder === 'asc' ? a.price - b.price : b.price - a.price
  );

  return (
    <div>
      <div className="controls">
        <label>
          カテゴリ:
          <select
            value={category}
            onChange={(e) => setCategory(e.target.value)}
          >
            <option value="all">すべて</option>
            <option value="PC">PC</option>
            <option value="周辺機器">周辺機器</option>
          </select>
        </label>

        <label>
          並び順:
          <select
            value={sortOrder}
            onChange={(e) => setSortOrder(e.target.value)}
          >
            <option value="asc">価格: 安い順</option>
            <option value="desc">価格: 高い順</option>
          </select>
        </label>
      </div>

      {sorted.length === 0
        ? <p>該当する商品がありません。</p>
        : (
          <ul>
            {sorted.map(product => (
              <li key={product.id}>
                {product.name} - {product.price.toLocaleString()}<span className="badge">{product.category}</span>
              </li>
            ))}
          </ul>
        )
      }

      <p>{sorted.length} 件の商品が見つかりました。</p>
    </div>
  );
}

export default FilterableProductList;

ポイント: ソート時に [...filtered].sort() のようにスプレッド構文で配列をコピーしてからソートしています。 sort() は元の配列を変更する破壊的メソッドのため、直接 filtered.sort() とすると State の不変性に反します。常に新しい配列を作ってから操作しましょう。