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)
- 受保護的元件(未登入時顯示登入頁面)
本課重點回顧
- Context 解決 Props Drilling 問題
- 三步驟:
createContext→Provider→useContext - 建立自訂 Hook 封裝 Context 使用邏輯
- 可以巢狀多個 Provider
- 拆分不同功能的 Context 避免效能問題
- 不要過度使用 — 簡單的 props 傳遞也很好