React useContext
useContext 是一個讓你在元件中讀取 Context 的 Hook。Context 提供了一種在元件樹中共享資料的方式,而不需要透過 props 一層一層傳遞。
什麼時候需要 Context?
當你有一些資料需要被很多元件使用時,例如:
- 當前的使用者資訊
- 主題(深色/淺色模式)
- 語言設定
- 全域狀態
如果不用 Context,你需要透過 props 一層一層往下傳(prop drilling),這會讓程式碼變得冗長且難以維護。
基本用法
使用 Context 分為三個步驟:
1. 建立 Context
import { createContext } from 'react'
// 建立 Context,可以給一個預設值
const ThemeContext = createContext('light')
2. 提供 Context 值
在父元件中使用 Context 包裹子元件,並提供值:
function App() {
const [theme, setTheme] = useState('dark')
return (
// 使用 Context 作為 Provider
<ThemeContext value={theme}>
<Header />
<Main />
<Footer />
</ThemeContext>
)
}
在 React 19 中,可以直接使用
<Context value={...}> 而不需要 <Context.Provider value={...}>。3. 使用 Context 值
在任何子元件中使用 useContext 讀取值:
import { useContext } from 'react'
function ThemedButton() {
// 讀取 Context 值
const theme = useContext(ThemeContext)
return <button className={theme}>我是 {theme} 主題的按鈕</button>
}
完整範例:主題切換
import { createContext, useContext, useState } from 'react'
// 1. 建立 Context
const ThemeContext = createContext(null)
// 2. 建立 Provider 元件
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
function toggleTheme() {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}
return <ThemeContext value={{ theme, toggleTheme }}>{children}</ThemeContext>
}
// 3. 建立自訂 Hook(推薦)
function useTheme() {
const context = useContext(ThemeContext)
if (context === null) {
throw new Error('useTheme 必須在 ThemeProvider 內使用')
}
return context
}
// 4. 使用 Context 的元件
function ThemedButton() {
const { theme, toggleTheme } = useTheme()
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
padding: '10px 20px',
border: '1px solid #ccc',
}}
>
切換主題(目前:{theme})
</button>
)
}
function Header() {
const { theme } = useTheme()
return (
<header
style={{
backgroundColor: theme === 'light' ? '#f0f0f0' : '#222',
color: theme === 'light' ? '#333' : '#fff',
padding: '20px',
}}
>
<h1>網站標題</h1>
<ThemedButton />
</header>
)
}
// 5. App 元件
function App() {
return (
<ThemeProvider>
<Header />
<main>
<p>主要內容</p>
<ThemedButton />
</main>
</ThemeProvider>
)
}
建立自訂 Hook 封裝 Context
為了更好的開發體驗,建議建立自訂 Hook 來封裝 Context 的使用:
// contexts/AuthContext.jsx
import { createContext, useContext, useState } from 'react'
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
function login(userData) {
setUser(userData)
}
function logout() {
setUser(null)
}
return <AuthContext value={{ user, login, logout }}>{children}</AuthContext>
}
// 自訂 Hook
export function useAuth() {
const context = useContext(AuthContext)
if (context === null) {
throw new Error('useAuth 必須在 AuthProvider 內使用')
}
return context
}
使用方式:
// App.jsx
import { AuthProvider } from './contexts/AuthContext'
function App() {
return (
<AuthProvider>
<Router />
</AuthProvider>
)
}
// UserProfile.jsx
import { useAuth } from './contexts/AuthContext'
function UserProfile() {
const { user, logout } = useAuth()
if (!user) {
return <p>請先登入</p>
}
return (
<div>
<p>歡迎,{user.name}!</p>
<button onClick={logout}>登出</button>
</div>
)
}
多個 Context
你可以使用多個 Context 來管理不同類型的資料:
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<MainContent />
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
)
}
在元件中使用:
function Dashboard() {
const { user } = useAuth()
const { theme } = useTheme()
const { language } = useLanguage()
return (
<div className={theme}>
<h1>
{language === 'zh' ? '歡迎' : 'Welcome'}, {user.name}
</h1>
</div>
)
}
Context 更新機制
當 Context 的值改變時,所有使用該 Context 的元件都會重新渲染。如果只是部分值改變,但你只用到其他值,元件仍然會重新渲染。
優化策略:拆分 Context
如果不同的值更新頻率不同,考慮拆分成多個 Context:
// ❌ 問題:theme 和 user 在同一個 Context
// 當 user 改變時,只使用 theme 的元件也會重新渲染
const AppContext = createContext({ theme: 'light', user: null })
// ✅ 解法:拆分成多個 Context
const ThemeContext = createContext('light')
const UserContext = createContext(null)
實際範例:購物車
import { createContext, useContext, useReducer } from 'react'
// 建立 Context
const CartContext = createContext(null)
// Reducer 處理購物車邏輯
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.find((item) => item.id === action.payload.id)
if (existingItem) {
return state.map((item) =>
item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item
)
}
return [...state, { ...action.payload, quantity: 1 }]
case 'REMOVE_ITEM':
return state.filter((item) => item.id !== action.payload)
case 'CLEAR_CART':
return []
default:
return state
}
}
// Provider 元件
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, [])
function addItem(item) {
dispatch({ type: 'ADD_ITEM', payload: item })
}
function removeItem(id) {
dispatch({ type: 'REMOVE_ITEM', payload: id })
}
function clearCart() {
dispatch({ type: 'CLEAR_CART' })
}
const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
return (
<CartContext value={{ cart, addItem, removeItem, clearCart, total }}>{children}</CartContext>
)
}
// 自訂 Hook
export function useCart() {
const context = useContext(CartContext)
if (context === null) {
throw new Error('useCart 必須在 CartProvider 內使用')
}
return context
}
// 使用範例
function ProductCard({ product }) {
const { addItem } = useCart()
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>加入購物車</button>
</div>
)
}
function CartSummary() {
const { cart, total, removeItem, clearCart } = useCart()
return (
<div>
<h2>購物車</h2>
{cart.length === 0 ? (
<p>購物車是空的</p>
) : (
<>
<ul>
{cart.map((item) => (
<li key={item.id}>
{item.name} x {item.quantity} - ${item.price * item.quantity}
<button onClick={() => removeItem(item.id)}>移除</button>
</li>
))}
</ul>
<p>總計: ${total}</p>
<button onClick={clearCart}>清空購物車</button>
</>
)}
</div>
)
}