花了一些篇幅分別看 Context API 與 Zustand 的用法,接下來讓我們模擬一個實際的例子來理解這兩種加上 Redux 的差異。假設我們要實現一個使用者管理系統,包含登入狀態、主題切換和通知管理。
Context API 的設計哲學可以用「最小化依賴,最大化內聚」來概括。好處是大多數狀態共享的需求都可以用 React 自身的能力來解決,不需要引入額外的複雜性。
function App() {
return (
<UserProvider>
<ThemeProvider>
<Dashboard />
</ThemeProvider>
</UserProvider>
)
}
function Dashboard() {
const { user, logout } = useContext(UserContext)
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<div className={`dashboard ${theme}`}>
<h1>Welcome {user?.name}</h1>
<button onClick={toggleTheme}>切換主題</button>
<button onClick={logout}>登出</button>
</div>
)
}
Context API 採用「廣播式」更新模式。當 Context 中的任何值發生變化時,所有使用該 Context 的組件都會重新渲染。這就像一個廣播電台,無論播放什麼內容,所有調頻到這個頻道的收音機都會響起。在主程式要注意的就是如兩天前所說要在適當的地方包上 Provider。
Zustand 將狀態管理透過一個 store 集中管理,直觀易用、同時具備處理複雜場景的能力。
與前者相比之下,無需Provider。雖然 store 看起來寫了很長一段,但好處是主程式只有「訂閱」了特定狀態片段的 Component 才會在該片段變化時重新渲染。
function Dashboard() {
const user = useAppStore(state => state.user)
const theme = useAppStore(state => state.theme)
const logout = useAppStore(state => state.logout)
const toggleTheme = useAppStore(state => state.toggleTheme)
return (
<div className={`dashboard ${theme}`}>
<h1>Welcome {user?.name}</h1>
<button onClick={toggleTheme}>切換主題</button>
<button onClick={logout}>登出</button>
</div>
)
}
Redux 利用嚴格的單向資料流和不可變更新,確保應用的狀態變化完全可控、可追蹤、可調試。
function App() {
return (
<Provider store={store}>
<Dashboard />
</Provider>
)
}
function Dashboard() {
const { user } = useSelector(state => state.user)
const { theme } = useSelector(state => state.theme)
const dispatch = useDispatch()
return (
<div className={`dashboard ${theme}`}>
<h1>Welcome {user?.name}</h1>
<button onClick={() => dispatch(toggleTheme())}>切換主題</button>
<button onClick={() => dispatch(logout())}>登出</button>
</div>
)
}
Redux 採用「選擇式」更新模式。通過 selector 函數的對比結果決定是否重新渲染,配合 React-Redux 的優化機制實現精確更新。
Context 最一開始是為了解決 prop drilling 問題而出現的 hook,因此仍適用於這個情況。另外也適合小到中型的應用程式,因為當需要追蹤的狀態變多,首先 Provider 會隨之增加,雖然這邊帶來的問題並不大,但一層層終究可能帶來 Provider hell。
一旦應用程式 (或是團隊) 的規模變大或是有需要常常更新的狀態,就該考慮使用 Zustand 或 Redux,尤其 Zustand 的學習曲線低,很好上手。加上先前提到的,只要其中一個 state 更新,整個 Context 都會更新連動 rerender 相關的所有 Component,因此會有效能浪費的問題,比較適用於狀態更新頻率低,比如說全域的設定。
像是一般常見的電商,這種中大型的應用,可能會有跨 Component 的狀態,在 Store 統一管理同步與非同步的狀態,真的很方便,還能同時兼顧開發的效率與程式碼的簡潔性。我自己平常主要就是使用 Zustand,搭配一些 Context 設定主題或是通知 (符合更新頻率低、跨域設定的特性)。
沒錯,這些工具是可以混著用的。
雖然這個我自己沒有用過,查了一下發現他的學習曲線高一些且使用上較複雜,但因為非常的穩定,很適合處理複雜的 buisness logic 與狀態追蹤。
這邊再提供一個 Claude 寫的範例,供我自己參考,因為我也沒用過 XD
// 複雜的業務邏輯處理
// orderSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const processOrder = createAsyncThunk(
'orders/process',
async ({ orderId, paymentData }, { getState, dispatch, rejectWithValue }) => {
try {
const state = getState()
const user = state.auth.user
// 複雜的業務邏輯
const order = await orderAPI.process({
orderId,
userId: user.id,
payment: paymentData
})
// 連鎖效應處理
dispatch(updateInventory(order.items))
dispatch(sendNotification(`訂單 ${orderId} 處理成功`))
return order
} catch (error) {
return rejectWithValue(error.message)
}
}
)
const orderSlice = createSlice({
name: 'orders',
initialState: {
orders: [],
currentOrder: null,
processing: false,
error: null,
filters: {
status: 'all',
dateRange: null
}
},
reducers: {
setFilter: (state, action) => {
state.filters = { ...state.filters, ...action.payload }
},
clearError: (state) => {
state.error = null
}
},
extraReducers: (builder) => {
builder
.addCase(processOrder.pending, (state) => {
state.processing = true
state.error = null
})
.addCase(processOrder.fulfilled, (state, action) => {
state.processing = false
state.currentOrder = action.payload
// 更新訂單列表
const index = state.orders.findIndex(o => o.id === action.payload.id)
if (index !== -1) {
state.orders[index] = action.payload
}
})
.addCase(processOrder.rejected, (state, action) => {
state.processing = false
state.error = action.payload
})
}
})
// 複雜的 selector
import { createSelector } from '@reduxjs/toolkit'
const selectOrders = state => state.orders.orders
const selectFilters = state => state.orders.filters
export const selectFilteredOrders = createSelector(
[selectOrders, selectFilters],
(orders, filters) => {
let filtered = orders
if (filters.status !== 'all') {
filtered = filtered.filter(order => order.status === filters.status)
}
if (filters.dateRange) {
filtered = filtered.filter(order => {
const orderDate = new Date(order.createdAt)
return orderDate >= filters.dateRange.start && orderDate <= filters.dateRange.end
})
}
return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
}
)
// 使用 middleware 處理副作用
const orderMiddleware = (store) => (next) => (action) => {
const result = next(action)
// 訂單狀態變化時的副作用
if (action.type === processOrder.fulfilled.type) {
// 發送分析事件
analytics.track('order_processed', {
orderId: action.payload.id,
value: action.payload.total
})
// 更新用戶積分
store.dispatch(updateUserPoints(action.payload.points))
}
return result
}
總之,基於應用規模的考量的話,總結如下
小型應用 (< 50 組件)
├── 狀態簡單 → Context API
└── 需要跨組件共享 → Zustand
中型應用 (50-200 組件)
├── 快速開發 → Zustand
├── 團隊經驗豐富 → Redux Toolkit
└── 性能敏感 → Zustand
大型應用 (> 200 組件)
├── 複雜業務邏輯 → Redux Toolkit
├── 多團隊協作 → Redux Toolkit
└── 簡單狀態管理 → Zustand
如果根據團隊成員的背景而選擇工具的話,也可以,大部分的原則還是 Zustand 優先啦! 因為真的很好用。明天的文章將會再提供 Zustand 更多的使用方法與模板,敬請期待!!
團隊新手較多、快速原型開發:Zustand > Context API > Redux
團隊經驗豐富、長期維護項目:Redux > Zustand > Context API
在狀態管理的世界裡,沒有一種方案能夠完美解決所有問題。每種方案都有其設計哲學和適用場景:
Context API 提供了最直接的解決方案,適合那些希望保持簡潔、避免外部依賴的項目。它的價值在於「夠用就好」的實用主義。Zustand 在簡潔性和功能性之間找到了完美的平衡點,適合大多數現代 React 應用。它的價值在於「簡單而不簡陋」的設計理念。Redux 提供了最完整和成熟的解決方案,適合那些需要嚴格狀態管理和強大調試能力的大型應用。它的價值在於「可預測性勝過一切」的架構理念。