条件付きレンダリングとリスト
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>
);
}
ローディング表示の例
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>
);
}
ネストした三項演算子(非推奨)
三項演算子をネスト(入れ子)にすると可読性が大きく低下します。 条件が複雑な場合は、早期リターンパターンや変数に分離する方法を使いましょう。
// 読みにくい! 避けるべきパターン
return (
<div>
{isLoading
? <p>読み込み中...</p>
: error
? <p>エラーが発生しました</p>
: <p>{data}</p>
}
</div>
);
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 && ... のように明示的にブール値に変換しましょう。
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() は配列の各要素に対して関数を実行し、その戻り値で新しい配列を作成します。
function FruitList() {
const fruits = ['りんご', 'バナナ', 'みかん', 'ぶどう'];
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
);
}
オブジェクトの配列を描画
実際のアプリケーションでは、オブジェクトの配列を描画することがほとんどです。
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 のルール
- 一意であること:兄弟要素間でユニークな値を使う(グローバルにユニークでなくてよい)
- 安定していること:レンダーのたびに変わらない値を使う
- 予測可能であること:ランダムに生成しない(
Math.random()は NG)
// 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 にしても問題ないケースは、以下のすべてを満たす場合に限られます。
- リストの並び替えが発生しない
- リストの途中への追加・削除がない(末尾への追加のみ)
- 各リストアイテムが State を持たない(純粋な表示のみ)
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>
);
}
実践例:フィルタリングとソート
実際のアプリケーションでは、リストの表示にフィルタリング(絞り込み)やソート(並び替え)を組み合わせることが多いです。 以下の例では、商品リストを「カテゴリでフィルタ」「価格でソート」する機能を実装しています。
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 の不変性に反します。常に新しい配列を作ってから操作しましょう。