12.1 Context 解決的問題 — Props Drilling

當深層巢狀的元件需要上層的資料時,一層一層傳 props 非常冗餘:

App (theme="dark")
  └── Layout (theme="dark")      ← 只是傳遞
        └── Main (theme="dark")  ← 只是傳遞
              └── Sidebar (theme="dark") ← 只是傳遞
                    └── ThemeButton (theme="dark") ← 真正需要

Context 讓你跳過中間層級,直接分享資料:

App (ThemeProvider: theme="dark")
  └── Layout
        └── Main
              └── Sidebar
                    └── ThemeButton ← 直接用 useContext 取得 theme

12.2 Context API 基本用法

步驟 1:建立 Context

import { createContext } from 'react';

const ThemeContext = createContext('light'); // 預設值

步驟 2:提供 Context(Provider)

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  );
}

步驟 3:消費 Context(useContext)

import { useContext } from 'react';

function ThemeButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{
      backgroundColor: theme === 'dark' ? '#333' : '#fff',
      color: theme === 'dark' ? '#fff' : '#333',
    }}>
      目前主題:{theme}
    </button>
  );
}

12.3 完整範例:主題切換

import { createContext, useContext, useState } from 'react';

// ============================================================
// 1. 建立 Context
// ============================================================
const ThemeContext = createContext();

// ============================================================
// 2. 建立 Provider 元件
// ============================================================
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  };

  const themes = {
    light: {
      background: '#ffffff',
      text: '#1a202c',
      cardBg: '#f7fafc',
      border: '#e2e8f0',
      primary: '#4299e1',
    },
    dark: {
      background: '#1a202c',
      text: '#e2e8f0',
      cardBg: '#2d3748',
      border: '#4a5568',
      primary: '#63b3ed',
    },
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme, colors: themes[theme] }}>
      {children}
    </ThemeContext.Provider>
  );
}

// ============================================================
// 3. 自訂 Hook(方便使用)
// ============================================================
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必須在 ThemeProvider 內使用');
  }
  return context;
}

// ============================================================
// 4. 使用 Context 的元件們
// ============================================================
function Header() {
  const { theme, toggleTheme, colors } = useTheme();

  return (
    <header style={{
      padding: '16px 32px',
      backgroundColor: colors.cardBg,
      borderBottom: `1px solid ${colors.border}`,
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'center',
    }}>
      <h1 style={{ color: colors.text, margin: 0 }}>My App</h1>
      <button
        onClick={toggleTheme}
        style={{
          padding: '8px 16px',
          borderRadius: '20px',
          border: 'none',
          backgroundColor: colors.primary,
          color: 'white',
          cursor: 'pointer',
        }}
      >
        {theme === 'light' ? '🌙 深色模式' : '☀️ 淺色模式'}
      </button>
    </header>
  );
}

function Card({ title, children }) {
  const { colors } = useTheme();

  return (
    <div style={{
      padding: '20px',
      borderRadius: '8px',
      backgroundColor: colors.cardBg,
      border: `1px solid ${colors.border}`,
      marginBottom: '16px',
    }}>
      <h3 style={{ color: colors.text, marginTop: 0 }}>{title}</h3>
      <div style={{ color: colors.text }}>{children}</div>
    </div>
  );
}

function MainContent() {
  const { colors } = useTheme();

  return (
    <main style={{ padding: '24px' }}>
      <Card title="關於 Context">
        <p>Context 讓你不必透過 Props Drilling,即可在元件樹中共享資料。</p>
      </Card>
      <Card title="主題切換">
        <p>點擊右上角的按鈕切換淺色/深色主題。</p>
        <p>所有元件都會自動更新顏色!</p>
      </Card>
    </main>
  );
}

// ============================================================
// 5. App — 用 Provider 包裹
// ============================================================
function App() {
  return (
    <ThemeProvider>
      <AppContent />
    </ThemeProvider>
  );
}

function AppContent() {
  const { colors } = useTheme();

  return (
    <div style={{
      minHeight: '100vh',
      backgroundColor: colors.background,
      transition: 'all 0.3s',
      fontFamily: 'sans-serif',
    }}>
      <Header />
      <MainContent />
    </div>
  );
}

export default App;

12.4 多個 Context 搭配使用

import { createContext, useContext, useState } from 'react';

// ============================================================
// 使用者 Context
// ============================================================
const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => setUser(userData);
  const logout = () => setUser(null);

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

function useUser() {
  const context = useContext(UserContext);
  if (!context) throw new Error('useUser 必須在 UserProvider 內使用');
  return context;
}

// ============================================================
// 語言 Context
// ============================================================
const LanguageContext = createContext();

const translations = {
  'zh-TW': { welcome: '歡迎', login: '登入', logout: '登出', settings: '設定' },
  'en': { welcome: 'Welcome', login: 'Login', logout: 'Logout', settings: 'Settings' },
  'ja': { welcome: 'ようこそ', login: 'ログイン', logout: 'ログアウト', settings: '設定' },
};

function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('zh-TW');

  const t = (key) => translations[language][key] || key;

  return (
    <LanguageContext.Provider value={{ language, setLanguage, t }}>
      {children}
    </LanguageContext.Provider>
  );
}

function useLanguage() {
  const context = useContext(LanguageContext);
  if (!context) throw new Error('useLanguage 必須在 LanguageProvider 內使用');
  return context;
}

// ============================================================
// 使用元件
// ============================================================
function Navbar() {
  const { user, login, logout } = useUser();
  const { language, setLanguage, t } = useLanguage();

  return (
    <nav style={{
      display: 'flex',
      justifyContent: 'space-between',
      padding: '12px 24px',
      backgroundColor: '#2d3748',
      color: 'white',
    }}>
      <span>{t('welcome')}{user ? `, ${user.name}` : ''}</span>
      <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
        <select
          value={language}
          onChange={(e) => setLanguage(e.target.value)}
          style={{ padding: '4px' }}
        >
          <option value="zh-TW">中文</option>
          <option value="en">English</option>
          <option value="ja">日本語</option>
        </select>
        {user ? (
          <button onClick={logout}>{t('logout')}</button>
        ) : (
          <button onClick={() => login({ name: 'Alice', email: 'alice@example.com' })}>
            {t('login')}
          </button>
        )}
      </div>
    </nav>
  );
}

// ============================================================
// App — 巢狀 Provider
// ============================================================
function App() {
  return (
    <LanguageProvider>
      <UserProvider>
        <Navbar />
      </UserProvider>
    </LanguageProvider>
  );
}

export default App;

12.5 Context 使用建議

建議 說明
建立自訂 Hook useTheme()useContext(ThemeContext) 更好用
加入錯誤檢查 在自訂 Hook 中檢查是否在 Provider 內使用
拆分 Context 不同功能用不同 Context,避免不必要的重新渲染
不要過度使用 只在真正需要跨多層共享時使用;2-3 層的 props 傳遞是正常的
Provider 放高層 但不要全部放在最高層;按需求就近提供

12.6 練習題

練習 1:購物車 Context

建立一個 CartContext,包含: - items:購物車內容 - addItem:新增商品 - removeItem:移除商品 - totalPrice:計算總金額

練習 2:認證 Context

建立一個 AuthContext,包含: - 登入/登出功能 - 使用者資訊 - 認證狀態(loading / authenticated / unauthenticated) - 受保護的元件(未登入時顯示登入頁面)


本課重點回顧

  1. Context 解決 Props Drilling 問題
  2. 三步驟:createContextProvideruseContext
  3. 建立自訂 Hook 封裝 Context 使用邏輯
  4. 可以巢狀多個 Provider
  5. 拆分不同功能的 Context 避免效能問題
  6. 不要過度使用 — 簡單的 props 傳遞也很好