条件付きレンダリングとリストレンダリング
条件付きレンダリングは信号機のようなもので、状態に基づいて何を表示するかを決定します。一方、リストレンダリングは流れ作業のようなもので、データのリストから同一のUIコンポーネントをバッチ処理で生成します。
1. 学習内容
- 三項演算子
? :を使用して表示内容を制御する &&および||を使用して、条件付きレンダリングを簡略化します.map()を使用してリストを表示する- 「key」属性の重要性と使用に関するルール
- 空の状態と読み込み中の状態の処理
2. 電子商取引プラットフォームのフロントエンド事情
(1) 課題:商品リストの6つの状態
アリスは、あるECプラットフォームの商品一覧ページの制作に取り組んでいます。このページには6つの表示状態があり、状態が変わるたびに、彼女は手動でDOMを操作しなければなりません:
JAVASCRIPT
function renderProductList(state) {
const container = document.getElementById('product-list')
container.innerHTML = ''
if (state.isLoading) {
container.innerHTML = '<div class="loading">Loading......</div>'
} else if (state.error) {
container.innerHTML = '<div class="error">Failed to load</div>'
} else if (!state.isLoggedIn) {
container.innerHTML = '<div>Please log in</div>'
} else if (state.products.length === 0) {
container.innerHTML = '<div>No products available</div>'
} else {
let html = ''
state.products.forEach(p => {
html += `<div class="card"><h3>${p.name}</h3><p>$${p.price}</p></div>`
})
container.innerHTML = html
}
}
アリスの質問:状態が変わるたびに、手動でコンテナをクリアしてHTMLを生成しなければなりません。たった30行のコードで、現在の状態を表示するというたった一つのことしかできていません。
(2) Reactにおける条件付きレンダリングの解決策
JSX
function ProductList({ user, products, isLoading, error }) {
// Each state is independent if Branch,No interference with one another
if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage message={error} />
if (!user) return <LoginPrompt />
if (products.length === 0) return <EmptyState />
return (
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
メリット:わずか15行のコードですべての状態を網羅でき、各分岐が一目で理解しやすい。
3. 条件付きレンダリングの3つの活用方法
| メソッド | 構文 | 使用例 | 備考 |
|---|---|---|---|
| 三項演算子 | condition ? A : B |
2つの選択肢から1つを選ぶ(「else」分岐を含む) | 三項演算子のネストは可読性を損なうため、2レベル以上は避ける |
論理演算と && |
condition && A |
条件が満たされた場合にのみ表示(「else」なし) | 0 && <Comp /> は 0 を返すため、> 0 を使用してください |
| 論理OR ` | ` | `value |
(1) 三項演算子 ? :
「2つの選択肢から1つを選ぶ」という状況に適したシナリオ:
JSX
function StatusBadge({ isActive }) {
return (
<span style={{ color: isActive ? '#52c41a' : '#ff4d4f' }}>
{isActive ? '🟢 Online' : '🔴 Offline'}
</span>
)
}
// isActive=true → <span>🟢 Online</span>
// isActive=false → <span>🔴 Offline</span>
(2) 論理と &&
「条件が満たされた場合にのみ表示し、そうでない場合は表示しない」という用途に適しています:
JSX
function Notification({ count }) {
return (
<div className="notification-icon">
🔔
{/* count > 0 A red dot appears,Otherwise, nothing will be displayed. */}
{count > 0 && (
<span className="badge">{count > 99 ? '99+' : count}</span>
)}
</div>
)
}
// count=0 → Show only 🔔
// count=5 → Show 🔔 and "5"
⚠️ 注意点:
{items.length && <List />} と記述しないでください。items.length が 0 の場合、0 && <List /> の結果は 0 となり、React はページ上に 0 をレンダリングしてしまいます。正しい記述方法は {items.length > 0 && <List />} です。
(3) 論理OR ||
「フォールバック値の提供」に適しています:
JSX
function UserProfile({ user }) {
return (
<div>
<h2>{user.nickname || 'Anonymous User'}</h2>
<p>{user.bio || 'This person is lazy.,Nothing was written...'}</p>
<p>Location:{user.location || 'Unknown'}</p>
</div>
)
}
(1) ▶ サンプル:種類の条件付きレンダリングの並列比較
JSX
// ============================================
// Example:Combined Use of Three Types of Conditional Rendering in the User Panel
// ============================================
function UserPanel({ user }) {
return (
<div className="panel">
{/* 1. Ternary:Choose one of the two */}
<h2>
{user.isVIP ? '⭐ Premium Member' : 'Regular User'}
</h2>
{/* 2. &&:Display only when met */}
{user.isVIP && (
<p>Membership Expiration:{user.vipExpireDate}</p>
)}
{/* 3. ||:Floor value */}
<p>Signature:{user.slogan || 'This person is really cool.,No signature'}</p>
{/* Combined Use:List of Tags */}
{user.tags && user.tags.length > 0 && (
<div>
{user.tags.map(tag => (
<span key={tag} style={{ background: '#f0f0f0', padding: '2px 8px', margin: '2px', borderRadius: '4px' }}>
{tag}
</span>
))}
</div>
)}
</div>
)
}
4. リストのレンダリング:.map()
(1) 基本的な使い方
Array.map() 配列を順に処理し、各要素に対して JSX を返す:
JSX
const fruits = ['Apple', 'Banana', 'Orange', 'Grapes']
function FruitList() {
return (
<ul>
{fruits.map(fruit => (
<li>{fruit}</li>
))}
</ul>
)
}
// Output:
// <ul>
// <li>Apple</li>
// <li>Banana</li>
// <li>Orange</li>
// <li>Grapes</li>
// </ul>
(2) レンダリングオブジェクトの配列
JSX
function ScoreTable() {
const students = [
{ id: 1, name: 'Alice', score: 92 },
{ id: 2, name: 'Bob', score: 85 },
{ id: 3, name: 'Charlie', score: 78 }
]
return (
<table border="1" cellPadding="8">
<thead>
<tr><th>Name</th><th>Fractions</th><th>Level</th></tr>
</thead>
<tbody>
{students.map(s => (
<tr key={s.id}>
<td>{s.name}</td>
<td>{s.score}</td>
<td>{s.score >= 90 ? 'Excellent' : s.score >= 80 ? 'Good' : 'Passing Grade'}</td>
</tr>
))}
</tbody>
</table>
)
}
(3) リスト表示のための3つのデータ変換手法
JSX
const items = [
{ id: 1, name: 'React Basics', price: 0 },
{ id: 2, name: 'Vue Getting Started', price: 0 },
{ id: 3, name: 'Node.js Real-World Experience', price: 99 }
]
// 1. Direct Rendering
{items.map(item => <li key={item.id}>{item.name}</li>)}
// 2. Filter First, Then Render(Free Courses)
{items
.filter(item => item.price === 0)
.map(item => <li key={item.id}>{item.name}(Free)</li>)
}
// 3. Extract Rendering Functions
function renderItem(item) {
return (
<li key={item.id}>
{item.name} - ${item.price}
</li>
)
}
{items.map(renderItem)}
5. 鍵の重要性
React は、リスト内の各要素を識別するために key を使用します。キーが変更されない場合、React はその DOM ノードを再利用します。キーが変更された場合、React はその DOM ノードを破棄して再作成します。
graph LR
A[List Changes] --> B{key Changes?}
B -->|No, key The same| C[Reuse DOM Node<br/>Update only the changes]
B -->|Yes, key It's changed| D[Delete Old Nodes<br/>Create a New Node]
B -->|No, None key| E[Render All Over]
| 主要戦略 | 実績 | 提言 |
|---|---|---|
| データベースの一意ID | ✅ 最適 | ⭐⭐⭐ |
| 一意の文字列(UUID) | ✅ 最高 | ⭐⭐⭐ |
| 配列のインデックス | ⚠️ リスクあり(リストが変更されると状態が不整合になる可能性がある) | ⭐ |
| 乱数 | ❌ 範囲(レンダリングごとに異なる) | ❌ |
| キーが渡されていない | ❌ パフォーマンスが低い | ❌ |
(1) ▶ サンプル:キーがある場合とない場合
JSX
// ============================================
// Example:key Impact on the Input Field's State
// ============================================
function KeyDemo() {
const [items, setItems] = React.useState([
{ id: 'a', text: 'Task A' },
{ id: 'b', text: 'Task B' },
{ id: 'c', text: 'Task C' }
])
function shuffle() {
setItems([...items].sort(() => Math.random() - 0.5))
}
return (
<div>
<button onClick={shuffle}>Randomize the order</button>
<p>✅ has key(id Stable,The content in the input field remains intact):</p>
<ul>
{items.map(item => (
<li key={item.id}>
{item.text} <input placeholder="Enter content" />
</li>
))}
</ul>
<p>❌ Without key(or index As key,The text in the input field appears jumbled):</p>
<ul>
{items.map((item, index) => (
<li key={index}>
{item.text} <input placeholder="The content may be misaligned" />
</li>
))}
</ul>
</div>
)
}
// Click"Randomize the order"after ,has key The content in the list input field remains in order
// use index List of,The content in the input field and the text are misaligned
6. 空の状態と読み込み中の状態
| ステータス | 状態 | UIの動作 | 対応 |
|---|---|---|---|
| 読み込み中 | loading === true |
スピナー/スケルトン画面 | if (loading) return <Spinner /> |
| 読み込みに失敗しました | error !== null |
エラーメッセージ + 再試行ボタン | if (error) return <ErrorView /> |
| 空のデータ | data.length === 0 |
空の状態のイラスト+コピー | if (!data.length) return <Empty /> |
| 通常 | 上記のいずれでもない | データ一覧を表示 | デフォルト:通常のUIに戻る |
実際のプロジェクトでは、データを読み込む際に処理しなければならない3つの状態があります:
JSX
// ============================================
// Example:Complete List Component(3 Handling Different States)
// ============================================
function ArticleList() {
const [articles, setArticles] = React.useState([])
const [isLoading, setIsLoading] = React.useState(true)
const [error, setError] = React.useState(null)
// Simulated Loading(Actually, it is API Call)
React.useEffect(() => {
fetch('/api/articles')
.then(res => {
if (!res.ok) throw new Error('Network request failed')
return res.json()
})
.then(data => {
setArticles(data)
setIsLoading(false)
})
.catch(err => {
setError(err.message)
setIsLoading(false)
})
}, [])
// 1. Loading...
if (isLoading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div className="spinner" />
<p>Loading article...</p>
</div>
)
}
// 2. Failed to load
if (error) {
return (
<div style={{ textAlign: 'center', padding: '40px', color: '#ff4d4f' }}>
<p>❌ Failed to load:{error}</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
)
}
// 3. Empty data
if (articles.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
<p>📭 No articles available</p>
<p>No one has posted an article yet.,Go ahead and write your first one!!</p>
</div>
)
}
// 4. Normal Rendering
return (
<div>
<h3>List of Articles(Total {articles.length} articles)</h3>
{articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
)
}
7. 完全な例:商品カテゴリと検索
JSX
// ============================================
// Complete Example:Product List(Conditional Rendering + Comprehensive Guide to List Rendering)
// Features:Filter by Category、Search、Prompt for empty results
// ============================================
function ShopPage() {
const [products] = React.useState([
{ id: 1, name: 'Wireless Mouse', category: 'Electronics', price: 89, sales: 2000 },
{ id: 2, name: 'Mechanical Keyboard', category: 'Electronics', price: 299, sales: 1500 },
{ id: 3, name: 'Notebook', category: 'Office', price: 15, sales: 5000 },
{ id: 4, name: 'Office Chair', category: 'Furniture', price: 899, sales: 800 },
{ id: 5, name: 'Desk Lamp', category: 'Furniture', price: 129, sales: 3000 },
{ id: 6, name: 'Monitor', category: 'Electronics', price: 1599, sales: 600 }
])
const [search, setSearch] = React.useState('')
const [category, setCategory] = React.useState('All')
// Filtering Logic
const filtered = products.filter(p => {
const matchSearch = p.name.includes(search)
const matchCategory = category === 'All' || p.category === category
return matchSearch && matchCategory
})
// Get the list of categories(Remove duplicates)
const categories = ['All', ...new Set(products.map(p => p.category))]
return (
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
<h2>🛒 Product List</h2>
{/* Search box */}
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search by Product Name..."
style={{ width: '100%', padding: '8px', marginBottom: '12px', border: '1px solid #d9d9d9', borderRadius: '4px' }}
/>
{/* Category Button(List Rendering) */}
<div style={{ marginBottom: '12px' }}>
{categories.map(cat => (
<button
key={cat}
onClick={() => setCategory(cat)}
style={{
padding: '6px 16px', margin: '0 4px',
backgroundColor: category === cat ? '#1890ff' : '#f0f0f0',
color: category === cat ? 'white' : '#333',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}
>
{cat}
</button>
))}
</div>
{/* Results Summary */}
{search || category !== 'All' ? (
<p style={{ color: '#666' }}>
{search && `Search"${search}" `}
{category !== 'All' && `/${category} `}
Total {filtered.length} Items
</p>
) : null}
{/* Empty result */}
{filtered.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
<p>😕 No matching products were found.</p>
<button
onClick={() => { setSearch(''); setCategory('All') }}
style={{ padding: '8px 16px', marginTop: '8px', cursor: 'pointer' }}
>
Clear All Filters
</button>
</div>
) : (
/* Product Grid(List Rendering) */
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
{filtered.map(product => (
<div key={product.id} style={{ border: '1px solid #eee', borderRadius: '8px', padding: '16px' }}>
<div style={{ height: '120px', backgroundColor: '#f5f5f5', borderRadius: '4px', marginBottom: '8px' }} />
<h4>{product.name}</h4>
<p style={{ color: '#ff4d4f', fontWeight: 'bold', fontSize: '18px' }}>${product.price}</p>
<p style={{ color: '#999', fontSize: '13px' }}>Sold {product.sales} items</p>
<span style={{ background: '#f0f5ff', color: '#1890ff', padding: '2px 8px', borderRadius: '4px', fontSize: '12px' }}>
{product.category}
</span>
</div>
))}
</div>
)}
</div>
)
}
期待される出力:検索フィルタリング、カテゴリフィルタリング、検索結果がない場合の通知、および統計機能を備えた、完全な商品一覧ページ。
(1) ▶ サンプル:フォームフィールドの動的レンダリング
JSX
function DynamicForm() {
const [formType, setFormType] = useState('personal')
const [formData, setFormData] = useState({
name: '',
email: '',
company: '',
taxId: '',
budget: '',
})
function handleChange(e) {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const personalFields = (
<>
<input name="name" value={formData.name} onChange={handleChange} placeholder="Name" />
<input name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
</>
)
const businessFields = (
<>
<input name="company" value={formData.company} onChange={handleChange} placeholder="Company" />
<input name="taxId" value={formData.taxId} onChange={handleChange} placeholder="Tax ID" />
<input name="budget" value={formData.budget} onChange={handleChange} placeholder="Budget ($)" />
</>
)
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Registration</h3>
<div style={{ marginBottom: 12 }}>
{['personal', 'business'].map(type => (
<button
key={type}
onClick={() => setFormType(type)}
style={{
padding: '6px 16px', marginRight: 8, cursor: 'pointer',
backgroundColor: formType === type ? '#1890ff' : '#f0f0f0',
color: formType === type ? 'white' : '#333', border: 'none', borderRadius: 4,
}}
>
{type === 'personal' ? 'Personal' : 'Business'}
</button>
))}
</div>
{formType === 'personal' ? personalFields : businessFields}
<button style={{ marginTop: 12, padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
</div>
)
}
(2) ▶ サンプル:リストの検索、フィルタリング、および並べ替え
JSX
function FilterableList() {
const [items] = useState([
{ id: 1, name: 'React', category: 'Frontend', stars: 220 },
{ id: 2, name: 'Django', category: 'Backend', stars: 78 },
{ id: 3, name: 'Vue', category: 'Frontend', stars: 207 },
{ id: 4, name: 'Express', category: 'Backend', stars: 63 },
{ id: 5, name: 'Tailwind', category: 'CSS', stars: 80 },
])
const [search, setSearch] = useState('')
const [sortKey, setSortKey] = useState('name')
const filtered = items
.filter(i => i.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => sortKey === 'stars' ? b.stars - a.stars : a.name.localeCompare(b.name))
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<input value={search} onChange={e => setSearch(e.target.value)}
placeholder="Search frameworks..." style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
<div style={{ marginBottom: 8 }}>
<button onClick={() => setSortKey('name')} style={{ padding: '4px 12px', marginRight: 4, cursor: 'pointer', background: sortKey === 'name' ? '#1890ff' : '#f0f0f0', color: sortKey === 'name' ? 'white' : '#333', border: 'none', borderRadius: 4 }}>A-Z</button>
<button onClick={() => setSortKey('stars')} style={{ padding: '4px 12px', cursor: 'pointer', background: sortKey === 'stars' ? '#1890ff' : '#f0f0f0', color: sortKey === 'stars' ? 'white' : '#333', border: 'none', borderRadius: 4 }}>Stars</button>
</div>
{filtered.length === 0 ? (
<p style={{ color: '#999', textAlign: 'center' }}>No results</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{filtered.map(i => <li key={i.id} style={{ padding: 8, borderBottom: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between' }}><span>{i.name} <small style={{ color: '#999' }}>({i.category})</small></span><span>⭐ {i.stars}k</span></li>)}
</ul>
)}
</div>
)
}
(3) ▶ サンプル:ネストされたリストのレンダリング—カテゴリ別商品の表示
JSX
function CategorizedProducts() {
const categories = [
{ id: 'electronics', name: 'Electronics', products: [
{ id: 1, name: 'Keyboard', price: 79 },
{ id: 2, name: 'Monitor', price: 399 },
]},
{ id: 'furniture', name: 'Furniture', products: [
{ id: 3, name: 'Desk', price: 249 },
{ id: 4, name: 'Chair', price: 189 },
]},
{ id: 'books', name: 'Books', products: [] },
]
return (
<div style={{ maxWidth: 500, margin: '0 auto' }}>
{categories.map(cat => (
<div key={cat.id} style={{ marginBottom: 16 }}>
<h3 style={{ borderBottom: '2px solid #1890ff', paddingBottom: 4 }}>{cat.name}</h3>
{cat.products.length === 0 ? (
<p style={{ color: '#999', fontStyle: 'italic', paddingLeft: 16 }}>No products yet</p>
) : (
<ul style={{ listStyle: 'none', padding: 0, paddingLeft: 16 }}>
{cat.products.map(p => (
<li key={p.id} style={{ padding: 4, display: 'flex', justifyContent: 'space-between' }}>
<span>{p.name}</span><span>${p.price}</span>
</li>
))}
</ul>
)}
</div>
))}
</div>
)
}
❓ よくある質問
Q
.map() はネストできますか?複数の .map() 呼び出しを使用する場合、キーはどのように扱われますか?A はい、ネストできます。
.map() の各レベルには、それぞれ独自のキーが必要です。例えば、「カテゴリー → 商品: cats.map(c => <div key={c.id}><h3>{c.name}</h3>{c.products.map(p => <span key={p.id}>{p.name}</span>)}</div>)」をレンダリングする場合などです。異なるレベルのキーは同じでも構いません(それぞれの配列内で一意である限り)。Q 三項演算子と && 構文、どちらが優れていますか?
A それぞれに適した用途があります。
&& は「条件が満たされた場合は表示し、満たされない場合は何も表示しない」場合に適しています。三項演算子 ? : は「条件 A が満たされた場合はコンポーネント X を表示し、条件 B が満たされた場合はコンポーネント Y を表示する」場合に適しています。三項演算子を使って「条件が満たされない場合は何も表示しない」ようにしたい場合、構文は condition ? <Comp /> : null になりますが、この場合は && の方が簡潔です。ただし、&& には「0 レンダリングの落とし穴」があることに注意してください。つまり、0 && <Comp /> は 0 をレンダリングしてしまいます。📖 まとめ
- 条件付きレンダリングには、
? :(いずれか一方)、&&(表示のみ)、||(フォールバック値)の3種類があります。 - リストのレンダリングには
.map()を使用してください。各 JSX 要素は、一意のkeyにバインドされている必要があります。 - 実際のプロジェクトでは、「読み込み中」、「読み込み失敗」、「データなし」の3つの状態を処理する必要があります。
{items.length && <List />}の0レンダリングの罠に注意- キーとしては一意で安定したIDを使用し、インデックスは使用しないでください
📝 練習問題
- 基本問題(難易度 ⭐):
tempのプロップスに基づいて、気温が35°Cより高い場合は赤色の🔥を、15~35°Cの場合は黄色の☀️を、15°Cより低い場合は青色の❄️を表示するTemperatureDisplayコンポーネントを作成してください。 - 上級問題(難易度 ⭐⭐):2つのプロップ(
todos(タスクの配列)とfilter(all/completed/uncompleted))を受け取るTodoFilterコンポーネントを作成し、状態に基づいたフィルタリングを実装してください。 - 課題(難易度:⭐⭐⭐):
Pagination、totalPages、currentPage、onPageChangeのプロパティを受け取り、ページ番号ボタンをレンダリングするPaginationコンポーネントを作成してください。このコンポーネントは、最初と最後のページ、および省略記号に対応している必要があります。



