Redux 很適合管理大型應用的複雜狀態,舉凡 POS 系統的訂單流程、交易所的開單、高併發應用的數據同步,都非常適合用 Redux Toolkit。但如果狀態簡單、應用規模小,useContext 或 Zustand 更會更合適。
特性 | Redux Toolkit | Zustand | useContext | Recoil |
|---|
DevTools 支援 | 優秀 | 一般 | 無 | 無 |
非同步操作 | Thunk/Saga | 簡單 | 需自行 | 有 |
適用規模 | 大型(500+) | 中型(100-500) | 小型(<100) | 中型 |
適合場景 | POS、交易所、高併發 | 聊天室、LLM | 簡單全局 | 細粒度 |
核心概念:createSlice
createSlice 是 Redux Toolkit 的核心,把狀態、reducer、action 整合在一起,同時內建 Immer,讓 reducer 中可以直接修改 state。
import { createSlice } from '@reduxjs/toolkit'
const orderSlice = createSlice({
name: 'orders',
initialState: { items: [], total: 0 },
reducers: {
addOrder: (state, action) => {
state.items.push(action.payload)
state.total += action.payload.amount
}
}
})原生 Redux 要手動做不可變複製,{ ...state, items: [...state.items, action.payload] } 這樣的寫法很容易出錯。Immer 讓你可以寫起來像是直接修改,而底層仍然產生新物件,reducer 純函數的特性也完整保留。
實例一:交易所開單與 createAsyncThunk
開單流程是個很好的例子,因為它同時有同步狀態(表單)和非同步操作(送出訂單到伺服器)。
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const submitOrder = createAsyncThunk(
'exchange/submitOrder',
async (orderData, { rejectWithValue }) => {
try {
const res = await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(orderData)
})
if (!res.ok) throw new Error('Order submission failed')
return await res.json()
} catch (error) {
return rejectWithValue(error.message)
}
}
)
const exchangeSlice = createSlice({
name: 'exchange',
initialState: {
orders: {},
account: { availableCash: 100000, frozenCash: 0 },
loading: false,
error: null
},
reducers: {
orderOpened: (state, action) => {
const order = action.payload
state.orders[order.orderId] = { ...order, status: 'open', filledQuantity: 0 }
if (order.side === 'buy') {
const amount = order.price * order.quantity
state.account.frozenCash += amount
state.account.availableCash -= amount
}
}
},
extraReducers: (builder) => {
builder
.addCase(submitOrder.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(submitOrder.fulfilled, (state) => {
state.loading = false
})
.addCase(submitOrder.rejected, (state, action) => {
state.loading = false
state.error = action.payload
})
}
})createAsyncThunk 自動處理三個狀態:pending、fulfilled、rejected,不需要手動管理 loading 旗標。rejectWithValue 讓你把伺服器錯誤訊息傳回 reducer,避免整個錯誤物件混入 state。
實例二:WebSocket 撮合結果與實時持倉更新
訂單送出後,伺服器撮合是非同步的,成交結果靠 WebSocket 推送。這裡 Redux 的優勢最明顯,因為同一份 state 要同時更新持倉、資金、訂單狀態。
orderPartiallyFilled: (state, action) => {
const { orderId, tradeId, price, quantity } = action.payload
const order = state.orders[orderId]
if (!order) return
order.filledQuantity += quantity
order.status = 'partially_filled'
const [base] = order.symbol.split('/')
const pos = state.positions[base] ??= { quantity: 0, averageEntryPrice: 0 }
if (order.side === 'buy') {
const totalCost = pos.quantity * pos.averageEntryPrice + quantity * price
pos.quantity += quantity
pos.averageEntryPrice = totalCost / pos.quantity
state.account.frozenCash -= price * quantity
} else {
pos.quantity -= quantity
state.account.availableCash += price * quantity
}
},
updatePrice: (state, action) => {
const { symbol, price } = action.payload
state.prices[symbol] = price
if (state.positions[symbol]) {
state.positions[symbol].currentPrice = price
}
}組件這邊接 WebSocket 很乾淨,只需要把推送的事件對應到 action:
useEffect(() => {
const ws = new WebSocket('wss://api.exchange.com/ws')
ws.onmessage = (event) => {
const { type, payload } = JSON.parse(event.data)
dispatch(exchangeSlice.actions[type]?.(payload))
}
return () => ws.close()
}, [dispatch])這樣寫的好處是,WebSocket 和 UI 完全解耦。之後加新的事件類型,只要在 slice 加一個 reducer,組件不用動。
實例三:Redux DevTools 時間旅行除錯
DevTools 是 Redux 真正無可取代的功能,在除錯複雜狀態時特別有價值。
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {
exchange: exchangeSlice.reducer,
portfolio: portfolioSlice.reducer
}
})安裝瀏覽器擴展後,可以看到每一個 action 的 before / after state diff,也可以直接跳回任何一個歷史狀態。高併發應用中,多個 WebSocket 事件在毫秒內連續觸發,如果持倉數字算錯了,用 DevTools 逐一回放 action 就能立刻找到哪個 reducer 出問題。Zustand 和 useContext 都做不到這件事。
Selector 記憶化
複雜計算放進 createSelector,否則每次 render 都重新執行一遍。
const portfolioValue = useSelector(state =>
Object.values(state.exchange.positions)
.reduce((sum, pos) => sum + pos.quantity * state.exchange.prices[pos.symbol], 0)
)
export const selectPortfolioValue = createSelector(
[state => state.exchange.positions, state => state.exchange.prices],
(positions, prices) =>
Object.values(positions).reduce(
(sum, pos) => sum + pos.quantity * (prices[pos.symbol] ?? 0), 0
)
)交易所場景中,updatePrice 每秒可能觸發幾十次,如果每次都重新算整個投資組合的市值,在持倉多的情況下會有明顯的性能問題。createSelector 用 memoization 解決這個問題,input selectors 的返回值沒變就直接回傳快取結果。
常見誤區
addOrder: async (state, action) => {
const res = await fetch('/api/orders')
state.items.push(res)
}
export const fetchOrders = createAsyncThunk('orders/fetch', async () => {
const res = await fetch('/api/orders')
return res.json()
})Reducer 是純函數,同樣的 input 必須產生同樣的 output。非同步操作有副作用,放進 reducer 會讓 DevTools 的時間旅行功能失效,測試也沒辦法寫。
Redux Toolkit vs 其他方案的 trade off
一個簡單的聊天室用 Redux 管理訊息列表,configureStore、createSlice、Provider 這些樣板加起來比功能本體還多,Zustand 五行就能完成同樣的事。
但是當應用狀態開始牽涉多個關聯資源,像是交易所的訂單、持倉、資金、實時行情全部互相影響,或是 POS 系統的購物車、折扣、庫存、結帳流程,Redux 的集中式 store 加上 DevTools 才真正體現價值。每一個 action 都有完整記錄,新加入的工程師打開 DevTools 就能看懂整個狀態流,這在大團隊協作上省了很多溝通成本。
參考資源