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)顯示不同功能選單的元件。


本課重點回顧

  1. if/else:適合 return 前的複雜邏輯
  2. 三元運算子:適合 JSX 內的二選一
  3. &&:適合「條件為真則顯示」
  4. 物件映射:取代多重 switch/if
  5. Guard Clause:處理載入/錯誤/空資料
  6. && 要注意數字 0 的陷阱