7.1 什麼是條件渲染?
根據不同的狀態或條件,顯示不同的 UI。
7.2 條件渲染的方式
方式 1:if/else(在 return 之前)
function Greeting({ isLoggedIn, username }) {
if (isLoggedIn) {
return (
<div>
<h1>歡迎回來,{username}!</h1>
<button>登出</button>
</div>
);
}
return (
<div>
<h1>請登入</h1>
<button>登入</button>
</div>
);
}
方式 2:三元運算子(在 JSX 內部)
function StatusBadge({ status }) {
return (
<span style={{
padding: '4px 12px',
borderRadius: '20px',
fontSize: '0.85rem',
fontWeight: 'bold',
backgroundColor: status === 'active' ? '#c6f6d5' : '#fed7d7',
color: status === 'active' ? '#22543d' : '#9b2c2c',
}}>
{status === 'active' ? '啟用中' : '已停用'}
</span>
);
}
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
{user.bio ? (
<p>{user.bio}</p>
) : (
<p style={{ color: '#999' }}>尚未填寫自我介紹</p>
)}
</div>
);
}
方式 3:&& 短路求值(條件為真時顯示)
function Notifications({ count }) {
return (
<div>
<h2>通知</h2>
{count > 0 && (
<span style={{
padding: '2px 8px',
borderRadius: '12px',
backgroundColor: '#e53e3e',
color: 'white',
fontSize: '0.8rem',
}}>
{count}
</span>
)}
</div>
);
}
注意
&&的陷阱:數字 0 會被渲染出來!
// ❌ 如果 count 是 0,會顯示 "0"
{count && <span>{count} 則訊息</span>}
// ✅ 明確用布林表達式
{count > 0 && <span>{count} 則訊息</span>}
// ✅ 或者轉成布林值
{!!count && <span>{count} 則訊息</span>}
方式 4:用變數儲存 JSX
function AlertMessage({ type, message }) {
let icon;
let bgColor;
let textColor;
switch (type) {
case 'success':
icon = '✅';
bgColor = '#c6f6d5';
textColor = '#22543d';
break;
case 'warning':
icon = '⚠️';
bgColor = '#fefcbf';
textColor = '#744210';
break;
case 'error':
icon = '❌';
bgColor = '#fed7d7';
textColor = '#9b2c2c';
break;
default:
icon = 'ℹ️';
bgColor = '#bee3f8';
textColor = '#2a4365';
}
return (
<div style={{
padding: '12px 16px',
borderRadius: '8px',
backgroundColor: bgColor,
color: textColor,
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
<span>{icon}</span>
<span>{message}</span>
</div>
);
}
方式 5:物件映射取代多重 if/switch
function StatusIcon({ status }) {
const statusConfig = {
pending: { icon: '⏳', label: '待處理', color: '#ed8936' },
processing: { icon: '🔄', label: '處理中', color: '#4299e1' },
completed: { icon: '✅', label: '已完成', color: '#48bb78' },
cancelled: { icon: '❌', label: '已取消', color: '#e53e3e' },
};
const config = statusConfig[status] || statusConfig.pending;
return (
<span style={{ color: config.color }}>
{config.icon} {config.label}
</span>
);
}
方式 6:提早 return(Guard Clause)
function UserDashboard({ user, isLoading, error }) {
if (isLoading) {
return <div>載入中...</div>;
}
if (error) {
return <div style={{ color: 'red' }}>錯誤:{error}</div>;
}
if (!user) {
return <div>找不到使用者</div>;
}
return (
<div>
<h1>歡迎,{user.name}!</h1>
<p>Email: {user.email}</p>
</div>
);
}
7.3 顯示/隱藏元素
CSS 顯示/隱藏 vs 條件渲染
import { useState } from 'react';
function ToggleExample() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? '隱藏' : '顯示'}
</button>
{/* 方法 1:條件渲染 — 從 DOM 中移除 */}
{isVisible && <p>我用條件渲染(從 DOM 移除/新增)</p>}
{/* 方法 2:CSS display — 仍在 DOM 中 */}
<p style={{ display: isVisible ? 'block' : 'none' }}>
我用 CSS display(保留在 DOM 中)
</p>
{/* 方法 3:CSS opacity — 保留空間 */}
<p style={{ opacity: isVisible ? 1 : 0, transition: 'opacity 0.3s' }}>
我用 CSS opacity(保留空間,有漸變效果)
</p>
{/* 方法 4:CSS visibility — 保留空間 */}
<p style={{ visibility: isVisible ? 'visible' : 'hidden' }}>
我用 CSS visibility(保留空間)
</p>
</div>
);
}
| 方法 | 從 DOM 移除 | 保留空間 | 有動畫 | 適用場景 |
|---|---|---|---|---|
| 條件渲染 | ✅ | ❌ | ❌ | 不常切換的內容 |
display: none |
❌ | ❌ | ❌ | 需要保持 state |
opacity: 0 |
❌ | ✅ | ✅ | 需要漸變效果 |
visibility: hidden |
❌ | ✅ | ❌ | 需要保持佈局 |
7.4 完整範例:多步驟表單
import { useState } from 'react';
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
plan: '',
cardNumber: '',
});
const updateField = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const nextStep = () => setStep((prev) => Math.min(prev + 1, 4));
const prevStep = () => setStep((prev) => Math.max(prev - 1, 1));
const containerStyle = {
maxWidth: '500px',
margin: '40px auto',
padding: '32px',
borderRadius: '16px',
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
fontFamily: 'sans-serif',
};
const inputStyle = {
width: '100%',
padding: '10px 14px',
border: '2px solid #e2e8f0',
borderRadius: '8px',
fontSize: '1rem',
marginBottom: '16px',
boxSizing: 'border-box',
};
return (
<div style={containerStyle}>
{/* 進度條 */}
<div style={{ display: 'flex', marginBottom: '32px' }}>
{[1, 2, 3, 4].map((s) => (
<div key={s} style={{ flex: 1, textAlign: 'center' }}>
<div style={{
width: '36px',
height: '36px',
borderRadius: '50%',
backgroundColor: step >= s ? '#4299e1' : '#e2e8f0',
color: step >= s ? 'white' : '#a0aec0',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
transition: 'all 0.3s',
}}>
{step > s ? '✓' : s}
</div>
<p style={{
fontSize: '0.75rem',
color: step >= s ? '#4299e1' : '#a0aec0',
marginTop: '4px',
}}>
{s === 1 && '個人資料'}
{s === 2 && '選擇方案'}
{s === 3 && '付款資訊'}
{s === 4 && '確認'}
</p>
</div>
))}
</div>
{/* 步驟 1:個人資料 */}
{step === 1 && (
<div>
<h2>步驟 1:個人資料</h2>
<input
style={inputStyle}
placeholder="姓名"
value={formData.name}
onChange={(e) => updateField('name', e.target.value)}
/>
<input
style={inputStyle}
placeholder="Email"
value={formData.email}
onChange={(e) => updateField('email', e.target.value)}
/>
<input
style={inputStyle}
placeholder="電話"
value={formData.phone}
onChange={(e) => updateField('phone', e.target.value)}
/>
</div>
)}
{/* 步驟 2:選擇方案 */}
{step === 2 && (
<div>
<h2>步驟 2:選擇方案</h2>
{['basic', 'pro', 'enterprise'].map((plan) => (
<div
key={plan}
onClick={() => updateField('plan', plan)}
style={{
padding: '16px',
border: `2px solid ${formData.plan === plan ? '#4299e1' : '#e2e8f0'}`,
borderRadius: '8px',
marginBottom: '12px',
cursor: 'pointer',
backgroundColor: formData.plan === plan ? '#ebf8ff' : 'white',
}}
>
<strong>
{plan === 'basic' && '基本方案 - NT$ 299/月'}
{plan === 'pro' && '專業方案 - NT$ 599/月'}
{plan === 'enterprise' && '企業方案 - NT$ 1,299/月'}
</strong>
</div>
))}
</div>
)}
{/* 步驟 3:付款資訊 */}
{step === 3 && (
<div>
<h2>步驟 3:付款資訊</h2>
<input
style={inputStyle}
placeholder="信用卡號碼"
value={formData.cardNumber}
onChange={(e) => updateField('cardNumber', e.target.value)}
/>
</div>
)}
{/* 步驟 4:確認 */}
{step === 4 && (
<div>
<h2>步驟 4:確認訂單</h2>
<div style={{ backgroundColor: '#f7fafc', padding: '16px', borderRadius: '8px' }}>
<p><strong>姓名:</strong>{formData.name}</p>
<p><strong>Email:</strong>{formData.email}</p>
<p><strong>電話:</strong>{formData.phone}</p>
<p><strong>方案:</strong>{formData.plan}</p>
<p><strong>卡號:</strong>****{formData.cardNumber.slice(-4)}</p>
</div>
</div>
)}
{/* 按鈕列 */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
{step > 1 && (
<button onClick={prevStep} style={{
padding: '10px 24px',
border: '1px solid #e2e8f0',
borderRadius: '8px',
backgroundColor: 'white',
cursor: 'pointer',
}}>
上一步
</button>
)}
<div style={{ marginLeft: 'auto' }}>
{step < 4 ? (
<button onClick={nextStep} style={{
padding: '10px 24px',
border: 'none',
borderRadius: '8px',
backgroundColor: '#4299e1',
color: 'white',
cursor: 'pointer',
fontWeight: 'bold',
}}>
下一步
</button>
) : (
<button onClick={() => alert('訂單已提交!')} style={{
padding: '10px 24px',
border: 'none',
borderRadius: '8px',
backgroundColor: '#48bb78',
color: 'white',
cursor: 'pointer',
fontWeight: 'bold',
}}>
提交訂單
</button>
)}
</div>
</div>
</div>
);
}
export default MultiStepForm;
7.5 練習題
練習 1:登入/註冊切換頁面
建立一個可以在「登入」和「註冊」表單之間切換的頁面。
練習 2:權限控制顯示
建立一個根據使用者角色(admin / editor / viewer)顯示不同功能選單的元件。
本課重點回顧
- if/else:適合 return 前的複雜邏輯
- 三元運算子:適合 JSX 內的二選一
&&:適合「條件為真則顯示」- 物件映射:取代多重 switch/if
- Guard Clause:處理載入/錯誤/空資料
&&要注意數字 0 的陷阱