useContext 和 Provider 模式是處理跨層級狀態的標準做法,過程中不用逐層傳遞 props,直接在組件中讀取全局狀態,但如果用錯了就會造成性能問題和難以預測的重新渲染。
useContext 讓組件有能力存取在上層 Provider 中定義的值,避免了深層嵌套時的 props drilling(層層傳遞 props)。但 Context 改變時,所有訂閱該 Context 的組件都會重新渲染,即使它們只用了其中一部分狀態,針對 useContext 性能優化就是這個議題。Provider 模式就是創建一個 Context,在上層組件用 Provider 包裹,下層組件通過 useContext 讀取。
特性 | Props Drilling | useContext |
|---|
傳遞方式 | 逐層傳遞 | 直接注入 |
性能 | 按需更新 | 整體重新渲染 |
何時用 | 淺層結構 | 深層或全局狀態 |
重新渲染控制 | 精細 | 需優化 |
useContext
useContext 需要配合 createContext 和 Provider 使用:
import { createContext, useContext } from 'react'
const ThemeContext = createContext()
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
return useContext(ThemeContext)
}上層組件用 ThemeProvider 包裹,下層組件用 useTheme Hook 讀取狀態,不用去逐層傳遞。
實例 1:使用者身份驗證和權限
在 POS 系統中,很多組件需要知道當前登入的使用者和他們的權限,這時候如果用 props drilling 會很麻煩。
'use client'
import { createContext, useContext, useState, useEffect } from 'react'
const AuthContext = createContext()
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [permissions, setPermissions] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchUser = async () => {
try {
const res = await fetch('/api/auth/me')
if (res.ok) {
const data = await res.json()
setUser(data.user)
setPermissions(data.permissions)
}
} catch (error) {
console.error('Failed to fetch user:', error)
} finally {
setLoading(false)
}
}
fetchUser()
}, [])
return (
<AuthContext.Provider value={{ user, permissions, loading }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
export function OrderList() {
const { user, permissions } = useAuth()
const canDeleteOrder = permissions.includes('delete_order')
return (
<div>
<h2>訂單列表</h2>
<p>當前使用者: {user?.name}</p>
{canDeleteOrder && <button>刪除訂單</button>}
</div>
)
}任何組件都可以呼叫 useAuth 獲取使用者資訊和權限,組件掛載時就會去自動檢查登入狀態。
實例 2:主題和語言切換
電商平台通常支援深色模式和多語言,這些設定影響全局,需要在很多組件中使用。
'use client'
import { createContext, useContext, useState } from 'react'
const LocalizationContext = createContext()
export function LocalizationProvider({ children }) {
const [theme, setTheme] = useState('light')
const [language, setLanguage] = useState('en')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
const switchLanguage = (lang) => {
setLanguage(lang)
}
return (
<LocalizationContext.Provider
value={{
theme,
language,
toggleTheme,
switchLanguage
}}
>
{children}
</LocalizationContext.Provider>
)
}
export function useLocalization() {
return useContext(LocalizationContext)
}
export function Header() {
const { theme, language, toggleTheme, switchLanguage } = useLocalization()
return (
<header style={{
backgroundColor: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#000'
}}>
<button onClick={toggleTheme}>
切換到 {theme === 'light' ? '深色' : '淺色'} 模式
</button>
<select value={language} onChange={(e) => switchLanguage(e.target.value)}>
<option value="en">English</option>
<option value="zh">中文</option>
<option value="ja">日本語</option>
</select>
</header>
)
}
export function Footer() {
const { theme } = useLocalization()
return (
<footer style={{
backgroundColor: theme === 'dark' ? '#222' : '#f0f0f0',
color: theme === 'dark' ? '#fff' : '#000'
}}>
Footer content
</footer>
)
}Header 和 Footer 都能讀取主題和語言設定,改變任何一個設定時,依賴它們的組件自動更新。
實例 3:購物車狀態
在電商平台中,購物車是全局狀態。結帳頁、側邊欄、價格總計都需要讀取購物車。
'use client'
import { createContext, useContext, useReducer } from 'react'
const CartContext = createContext()
const initialCart = {
items: [],
totalPrice: 0,
itemCount: 0
}
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(item => item.id === action.payload.id)
if (existing) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
itemCount: state.itemCount + 1,
totalPrice: state.totalPrice + action.payload.price
}
}
return {
items: [...state.items, { ...action.payload, quantity: 1 }],
itemCount: state.itemCount + 1,
totalPrice: state.totalPrice + action.payload.price
}
}
case 'REMOVE_ITEM': {
const item = state.items.find(i => i.id === action.payload)
return {
items: state.items.filter(i => i.id !== action.payload),
itemCount: state.itemCount - (item?.quantity || 0),
totalPrice: state.totalPrice - (item ? item.price * item.quantity : 0)
}
}
case 'UPDATE_QUANTITY': {
const item = state.items.find(i => i.id === action.payload.id)
const priceDiff = (action.payload.quantity - item.quantity) * item.price
return {
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: action.payload.quantity }
: i
),
itemCount: state.itemCount + (action.payload.quantity - item.quantity),
totalPrice: state.totalPrice + priceDiff
}
}
case 'CLEAR':
return initialCart
default:
return state
}
}
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, initialCart)
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
)
}
export function useCart() {
return useContext(CartContext)
}
export function ShoppingCart() {
const { cart, dispatch } = useCart()
return (
<div>
<h2>購物車({cart.itemCount} 件)</h2>
<ul>
{cart.items.map(item => (
<li key={item.id}>
{item.name} - ${item.price} x {item.quantity}
<button onClick={() => dispatch({
type: 'UPDATE_QUANTITY',
payload: { id: item.id, quantity: item.quantity - 1 }
})}>
減少
</button>
<button onClick={() => dispatch({
type: 'REMOVE_ITEM',
payload: item.id
})}>
刪除
</button>
</li>
))}
</ul>
<p>總金額: ${cart.totalPrice}</p>
</div>
)
}
export function Checkout() {
const { cart } = useCart()
return (
<div>
<h2>結帳</h2>
<p>您購買了 {cart.itemCount} 件商品</p>
<p>總金額: ${cart.totalPrice}</p>
</div>
)
}購物車狀態由 CartProvider 管理,ShoppingCart 和 Checkout 都能存取,這樣在修改購物車時,兩個組件都會自動更新。
Context 的性能問題和 useContext 記憶化
Context 最大的問題是重新渲染失去控制,也是 useContext 優化性能的過程中最關鍵的一點。當 Context value 改變時,所有訂閱該 Context 的組件都會重新渲染,即使它們只用了 value 的一部分,這種 React Context 重新渲染問題在大型系統中特別明顯。
const value = {
user,
theme,
language,
permissions,
cart,
updateCart,
setTheme
}
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
解決方法是拆分 Context,針對不同用途的狀態用不同的 Context,可以精細控制重新渲染的範圍。
const AuthContext = createContext()
const ThemeContext = createContext()
const CartContext = createContext()
export function AppProvider({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</AuthProvider>
)
}每個 Context 只管理相關的狀態。改變主題時只會重新渲染 ThemeContext 的訂閱者,不會影響 AuthContext 或 CartContext 的訂閱者。
常見錯誤
誤區 1:把所有狀態放在一個 Context
const AppContext = createContext()
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const [cart, setCart] = useState([])
const [notifications, setNotifications] = useState([])
return (
<AppContext.Provider value={{ user, theme, cart, notifications }}>
{children}
</AppContext.Provider>
)
}任何狀態改變都會觸發整個 App 的重新渲染。這會嚴重影響性能。
誤區 2:Context value 沒有記憶化
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
)
}
這是 React Context 重新渲染的常見原因,正確的 useContext 記憶化做法是用 useMemo:
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const value = useMemo(() => ({ user, setUser }), [user])
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}誤區 3:在 Context 中做複雜邏輯
export function CartProvider({ children }) {
const [cart, setCart] = useState([])
const addToCart = (product) => {
fetch('/api/add-to-cart', { body: JSON.stringify(product) })
.then(res => res.json())
.then(data => setCart(data))
}
return (
<CartContext.Provider value={{ cart, addToCart }}>
{children}
</CartContext.Provider>
)
}Context 應該保持簡單,複雜邏輯應該在 useReducer 或自訂 Hook 中。
何時選擇 useContext vs Redux vs Zustand
特性 | useContext | Redux | Zustand |
|---|
性能表現 | 需優化 | 優秀 | 優秀 |
重新渲染控制 | 粗糙 | 精細 | 精細 |
DevTools 支援 | 無 | 優秀 | 有 |
包大小 | 0KB(內建) | 2.2KB | 1.2KB |
非同步操作 | 需自行處理 | Middleware 完善 | 簡單 |
時間旅行除錯 | 無 | 有 | 有 |
適用場景 | 簡單全局狀態 | 大型複雜應用 | 中型應用、快速開發 |
useContext 記憶化難度 | 中等 | 無需考慮 | 無需考慮 |
useContext 最適合的場景
應用規模小(< 100 個組件)
狀態改變不頻繁(登入狀態、主題、語言)
不需要復雜的中間件或時間旅行除錯
優先考慮減少依賴和包大小
例:個人部落格、中小型電商平台、POS 收銀系統的用戶身份和權限
Redux 最適合的場景
應用規模大(> 500 個組件)
狀態轉換複雜且需要記錄每一步
需要強大的 DevTools 和時間旅行除錯
多人協作開發,需要明確的狀態架構
例:金融平台、大型電商、企業級 SaaS 應用
Zustand 最適合的場景
應用規模中等(100-500 個組件)
想要性能優化但不想學習複雜 API
需要簡單的非同步操作支援
青睞函數式而不是 action/reducer 模式
例:中型電商、設計工具、實時協作應用
實際建議
在 Next.js 中:
使用者身份、權限、主題 → useContext(必須拆分 Context)
購物車、訂單狀態 → 先試 useContext + useReducer,卡頓再考慮 Zustand
複雜業務邏輯、大量即時數據 → Redux 或 Zustand
伺服器狀態(API 數據)→ React Query / TanStack Query(不是 useContext)
參考資源