Blog
/
Frontend
Redux Toolkit
Redux 很適合管理大型應用的複雜狀態,舉凡 POS 系統的訂單流程、交易所的開單、高併發應用的數據同步,都非常適合用 Redux Toolkit。

Posted at
2026年3月23日
Posted on
Frontend
Redux 很適合管理大型應用的複雜狀態,舉凡 POS 系統的訂單流程、交易所的開單、高併發應用的數據同步,都非常適合用 Redux Toolkit。但如果狀態簡單、應用規模小,useContext 或 Zustand 更會更合適。
特性 | Redux Toolkit | Zustand | useContext | Recoil |
|---|---|---|---|---|
DevTools 支援 | 優秀 | 一般 | 無 | 無 |
非同步操作 | Thunk/Saga | 簡單 | 需自行 | 有 |
適用規模 | 大型(500+) | 中型(100-500) | 小型(<100) | 中型 |
適合場景 | POS、交易所、高併發 | 聊天室、LLM | 簡單全局 | 細粒度 |
Redux Toolkit 基礎概念
Redux Toolkit 的核心是 createSlice。一個 slice 包含狀態、reducer 和 action,完全替代了原生 Redux 的冗長寫法。
import { createSlice } from '@reduxjs/toolkit' const orderSlice = createSlice({ name: 'orders', initialState: { items: [], total: 0, status: 'idle' }, reducers: { addOrder: (state, action) => { // Immer 讓你可以直接修改 state state.items.push(action.payload) state.total += action.payload.amount }, removeOrder: (state, action) => { state.items = state.items.filter(order => order.id !== action.payload) } } }) export const { addOrder, removeOrder } = orderSlice.actions export default orderSlice.reducer
Redux Toolkit 內建 Immer,所以 reducer 中你可以直接修改 state,不需要手動做不可變更新。
實例:交易所開單、撮合交易和實時持倉更新
開單的核心流程是:用戶開單 → 伺服器撮合 → WebSocket 推送成交結果 → 客戶端更新持倉。Redux 需要管理整個客戶端狀態:開單表單、待開訂單、已成交訂單、實時持倉、可用資金。
import { createSlice, createAsyncThunk, configureStore } 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() // 返回 { orderId, symbol, side, price, quantity, status: 'open' } } catch (error) { return rejectWithValue(error.message) } } ) // 取消訂單 export const cancelOrder = createAsyncThunk( 'exchange/cancelOrder', async (orderId, { rejectWithValue }) => { try { const res = await fetch(`/api/orders/${orderId}`, { method: 'DELETE' }) if (!res.ok) throw new Error('Cancel failed') return await res.json() // 返回 { orderId, cancelledAt } } catch (error) { return rejectWithValue(error.message) } } ) const exchangeSlice = createSlice({ name: 'exchange', initialState: { // 開單表單狀態 orderForm: { symbol: 'BTC/USDT', side: 'buy', // 'buy' 或 'sell' price: 47000, quantity: 0.5, totalCost: 0, errors: {} }, // 訂單和交易 orders: { byId: {}, // { orderId: { orderId, symbol, side, price, quantity, status, filledQuantity } } allIds: [] }, trades: { byId: {}, // { tradeId: { tradeId, orderId, symbol, price, quantity, timestamp } } allIds: [] }, // 持倉和資金 positions: { 'BTC': { symbol: 'BTC', quantity: 2, averageEntryPrice: 45000, currentPrice: 47000 }, 'ETH': { symbol: 'ETH', quantity: 10, averageEntryPrice: 3000, currentPrice: 3100 } }, // 賬戶資訊 account: { totalBalance: 100000, availableCash: 23500, frozenCash: 0, // 掛單凍結資金 equity: 100000 // 實時權益 }, // 實時行情(來自 WebSocket) prices: { 'BTC': 47000, 'ETH': 3100 }, // 加載和錯誤狀態 loading: false, error: null }, reducers: { // 表單輸入 setOrderFormField: (state, action) => { const { field, value } = action.payload state.orderForm[field] = value // 自動計算總成本 if (field === 'price' || field === 'quantity') { const totalCost = state.orderForm.price * state.orderForm.quantity state.orderForm.totalCost = totalCost // 驗證餘額 if (state.orderForm.side === 'buy' && totalCost > state.account.availableCash) { state.orderForm.errors.insufficientFunds = `需要 ${totalCost} USDT,可用 ${state.account.availableCash}` } else { state.orderForm.errors.insufficientFunds = null } } }, // 清空表單 resetOrderForm: (state) => { state.orderForm = { symbol: 'BTC/USDT', side: 'buy', price: 0, quantity: 0, totalCost: 0, errors: {} } }, // WebSocket:訂單開啟 orderOpened: (state, action) => { const order = action.payload state.orders.byId[order.orderId] = { ...order, status: 'open', filledQuantity: 0, createdAt: new Date().toISOString() } state.orders.allIds.push(order.orderId) // 凍結資金(買單) if (order.side === 'buy') { state.account.frozenCash += order.price * order.quantity state.account.availableCash -= order.price * order.quantity } }, // WebSocket:訂單部分成交 orderPartiallyFilled: (state, action) => { const { orderId, tradeId, price, quantity, timestamp } = action.payload const order = state.orders.byId[orderId] if (!order) return order.filledQuantity += quantity order.status = 'partially_filled' // 記錄成交 state.trades.byId[tradeId] = { tradeId, orderId, symbol: order.symbol, side: order.side, price, quantity, timestamp } state.trades.allIds.push(tradeId) // 更新持倉 const [baseSymbol] = order.symbol.split('/') if (!state.positions[baseSymbol]) { state.positions[baseSymbol] = { symbol: baseSymbol, quantity: 0, averageEntryPrice: 0, currentPrice: price } } const pos = state.positions[baseSymbol] if (order.side === 'buy') { const totalCost = pos.quantity * pos.averageEntryPrice + quantity * price pos.quantity += quantity pos.averageEntryPrice = totalCost / pos.quantity } else if (order.side === 'sell') { pos.quantity -= quantity } // 更新資金 if (order.side === 'buy') { // 買單:實際成交時,凍結資金減少,現金也減少 state.account.frozenCash -= price * quantity } else { // 賣單:收到現金 state.account.availableCash += price * quantity } // 更新權益 state.account.equity = state.account.availableCash + state.account.frozenCash + Object.values(state.positions).reduce((sum, pos) => sum + pos.quantity * state.prices[pos.symbol], 0) }, // WebSocket:訂單全部成交 orderFilled: (state, action) => { const { orderId } = action.payload const order = state.orders.byId[orderId] if (order) { order.status = 'filled' } }, // WebSocket:訂單已取消 orderCancelled: (state, action) => { const { orderId } = action.payload const order = state.orders.byId[orderId] if (order) { order.status = 'cancelled' // 解凍資金 if (order.side === 'buy') { const unfrozenAmount = order.price * (order.quantity - order.filledQuantity) state.account.frozenCash -= unfrozenAmount state.account.availableCash += unfrozenAmount } } }, // 實時價格更新(來自 WebSocket) updatePrice: (state, action) => { const { symbol, price } = action.payload state.prices[symbol] = price // 更新持倉的當前價格 if (state.positions[symbol]) { state.positions[symbol].currentPrice = price } // 實時更新權益 state.account.equity = state.account.availableCash + state.account.frozenCash + Object.values(state.positions).reduce((sum, pos) => sum + pos.quantity * (state.prices[pos.symbol] || 0), 0) } }, extraReducers: (builder) => { builder .addCase(submitOrder.pending, (state) => { state.loading = true state.error = null }) .addCase(submitOrder.fulfilled, (state, action) => { state.loading = false // 伺服器返回訂單 ID,接下來靠 WebSocket 推送訂單狀態 }) .addCase(submitOrder.rejected, (state, action) => { state.loading = false state.error = action.payload }) .addCase(cancelOrder.fulfilled, (state, action) => { // 伺服器確認取消,WebSocket 會推送 orderCancelled }) } }) export const { setOrderFormField, resetOrderForm, orderOpened, orderPartiallyFilled, orderFilled, orderCancelled, updatePrice } = exchangeSlice.actions const store = configureStore({ reducer: { exchange: exchangeSlice.reducer } }) export default store
在組件中使用開單表單和實時更新:
'use client' import { useDispatch, useSelector, useRef, useEffect } from 'react-redux' import { useCallback } from 'react' import { setOrderFormField, resetOrderForm, submitOrder, orderOpened, orderPartiallyFilled, orderCancelled, updatePrice } from './store' export function OrderForm() { const dispatch = useDispatch() const form = useSelector(state => state.exchange.orderForm) const account = useSelector(state => state.exchange.account) const loading = useSelector(state => state.exchange.loading) const wsRef = useRef(null) // 連接 WebSocket 監聽實時撮合和行情 useEffect(() => { wsRef.current = new WebSocket('wss://api.exchange.com/ws') wsRef.current.onmessage = (event) => { const data = JSON.parse(event.data) switch (data.type) { case 'order_opened': dispatch(orderOpened(data.payload)) break case 'order_partially_filled': dispatch(orderPartiallyFilled(data.payload)) break case 'order_filled': dispatch(orderFilled(data.payload)) break case 'order_cancelled': dispatch(orderCancelled(data.payload)) break case 'price_update': dispatch(updatePrice(data.payload)) break default: break } } return () => { wsRef.current?.close() } }, [dispatch]) const handleSubmit = async (e) => { e.preventDefault() // 驗證表單 if (form.quantity <= 0 || form.price <= 0) { alert('請輸入有效的數量和價格') return } if (form.side === 'buy' && form.totalCost > account.availableCash) { alert('餘額不足') return } // 提交訂單到伺服器 await dispatch(submitOrder({ symbol: form.symbol, side: form.side, price: form.price, quantity: form.quantity })) // 開單成功後清空表單 dispatch(resetOrderForm()) } return ( <div> <h2>開單</h2> <form onSubmit={handleSubmit}> <select value={form.side} onChange={(e) => dispatch(setOrderFormField({ field: 'side', value: e.target.value }))} > <option value="buy">買入</option> <option value="sell">賣出</option> </select> <input type="number" placeholder="數量" value={form.quantity} onChange={(e) => dispatch(setOrderFormField({ field: 'quantity', value: parseFloat(e.target.value) }))} step="0.0001" /> <input type="number" placeholder="價格" value={form.price} onChange={(e) => dispatch(setOrderFormField({ field: 'price', value: parseFloat(e.target.value) }))} step="0.01" /> <p>總成本: {form.totalCost.toFixed(2)} USDT</p> <p>可用資金: {account.availableCash.toFixed(2)} USDT</p> {form.errors.insufficientFunds && ( <p style={{ color: 'red' }}>{form.errors.insufficientFunds}</p> )} <button type="submit" disabled={loading || Object.keys(form.errors).length > 0}> {loading ? '提交中...' : '提交訂單'} </button> </form> </div> ) } export function PositionSummary() { const positions = useSelector(state => state.exchange.positions) const prices = useSelector(state => state.exchange.prices) const account = useSelector(state => state.exchange.account) const unrealizedPnL = Object.values(positions).reduce((sum, pos) => { const currentValue = pos.quantity * (prices[pos.symbol] || 0) const entryValue = pos.quantity * pos.averageEntryPrice return sum + (currentValue - entryValue) }, 0) return ( <div> <h2>持倉</h2> <p>權益: {account.equity.toFixed(2)} USDT</p> <p>可用資金: {account.availableCash.toFixed(2)} USDT</p> <p>凍結資金: {account.frozenCash.toFixed(2)} USDT</p> <p>未實現盈虧: {unrealizedPnL.toFixed(2)} USDT</p> <table> <thead> <tr> <th>資產</th> <th>數量</th> <th>成本價</th> <th>現價</th> <th>市值</th> <th>盈虧</th> </tr> </thead> <tbody> {Object.values(positions).map(pos => { const currentValue = pos.quantity * (prices[pos.symbol] || 0) const entryValue = pos.quantity * pos.averageEntryPrice const pnl = currentValue - entryValue return ( <tr key={pos.symbol}> <td>{pos.symbol}</td> <td>{pos.quantity}</td> <td>{pos.averageEntryPrice}</td> <td>{prices[pos.symbol] || 0}</td> <td>{currentValue.toFixed(2)}</td> <td style={{ color: pnl >= 0 ? 'green' : 'red' }}>{pnl.toFixed(2)}</td> </tr> ) })} </tbody> </table> </div> ) }
整個過程 Redux 管理:
開單:表單狀態管理、驗證、凍結資金計算
提交:createAsyncThunk 發送訂單到伺服器
撮合:WebSocket 接收伺服器推送的成交結果
更新:持倉、資金、權益實時更新
實時行情:價格變化自動更新持倉市值
Redux Toolkit 的強大功能
1. Redux DevTools 時間旅行除錯
Redux DevTools 是 Redux 最強大的功能,可以看到每一個 action 改變了什麼,還能回到任何一個狀態。在除錯複雜的狀態變化時,這是無價的。
const store = configureStore({ reducer: { order: orderSlice.reducer, portfolio: portfolioSlice.reducer } // DevTools 自動整合,無需額外配置 })
2. 中間件系統
Redux Toolkit 的 middleware 讓你在 action 分發和 reducer 執行之間攔截和處理邏輯,對日誌、分析、錯誤追蹤很有用。
const customMiddleware = (store) => (next) => (action) => { console.log('Dispatching:', action) const result = next(action) console.log('New state:', store.getState()) return result } const store = configureStore({ reducer: { order: orderSlice.reducer }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(customMiddleware) })
3. Immer 不可變更新
Redux Toolkit 內建 Immer,讓你在 reducer 中直接修改 state,無需手動做不可變複製。
// 不需要 ...state, ...state.items 這樣的複製 addItem: (state, action) => { state.items.push(action.payload) // 直接修改 state.total += action.payload.price // Immer 會自動轉換成不可變更新 }
常見誤區
誤區 1:過度規範化導致複雜性
// 不好:過度規範化,邏輯複雜 { orderIds: [1, 2, 3], ordersById: { 1: { id: 1, items: [1, 2], total: 100 }, 2: { ... } }, itemIds: [1, 2, 3], itemsById: { 1: { id: 1, name: 'Product' }, 2: { ... } } }
誤區 2:在 reducer 中做非同步操作
// 不好 addOrder: async (state, action) => { const res = await fetch('/api/orders') // 錯誤:reducer 必須是純函數 state.items.push(res) } // 好:用 createAsyncThunk export const submitOrder = createAsyncThunk(...)
Reducer 必須是純函數,所有非同步操作都用 createAsyncThunk 或 middleware 處理。
誤區 3:Selector 沒有記憶化導致性能問題
// 不好 const portfolioValue = useSelector(state => { return Object.values(state.portfolio.positions).reduce((...) // 每次都重新計算 }) // 好:用 createSelector 記憶化 export const selectPortfolioValue = createSelector( [selectPositions, selectPrices], (positions, prices) => { ... } )


