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

簡單全局

細粒度

核心概念: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)        // 直接修改,Immer 自動轉換
      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
        // 伺服器確認送出,後續靠 WebSocket 推送訂單狀態
      })
      .addCase(submitOrder.rejected, (state, action) => {
        state.loading = false
        state.error = action.payload
      })
  }
})

createAsyncThunk 自動處理三個狀態:pendingfulfilledrejected,不需要手動管理 loading 旗標。rejectWithValue 讓你把伺服器錯誤訊息傳回 reducer,避免整個錯誤物件混入 state。

實例二:WebSocket 撮合結果與實時持倉更新

訂單送出後,伺服器撮合是非同步的,成交結果靠 WebSocket 推送。這裡 Redux 的優勢最明顯,因為同一份 state 要同時更新持倉、資金、訂單狀態。

// 在 exchangeSlice 的 reducers 中加入
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)
    // WebSocket 事件名直接對應 Redux action
    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
  }
  // DevTools 自動整合,開發環境無需額外設定
})

安裝瀏覽器擴展後,可以看到每一個 action 的 before / after state diff,也可以直接跳回任何一個歷史狀態。高併發應用中,多個 WebSocket 事件在毫秒內連續觸發,如果持倉數字算錯了,用 DevTools 逐一回放 action 就能立刻找到哪個 reducer 出問題。Zustand 和 useContext 都做不到這件事。

Selector 記憶化

複雜計算放進 createSelector,否則每次 render 都重新執行一遍。

// 不好:每次 render 都重新計算整個持倉市值
const portfolioValue = useSelector(state =>
  Object.values(state.exchange.positions)
    .reduce((sum, pos) => sum + pos.quantity * state.exchange.prices[pos.symbol], 0)
)

// 好:只有 positions 或 prices 真的改變才重新計算
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 的返回值沒變就直接回傳快取結果。

常見誤區

// 錯:reducer 必須是純函數
addOrder: async (state, action) => {
  const res = await fetch('/api/orders')
  state.items.push(res)
}

// 對:非同步操作一律放 createAsyncThunk
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 管理訊息列表,configureStorecreateSliceProvider 這些樣板加起來比功能本體還多,Zustand 五行就能完成同樣的事。

但是當應用狀態開始牽涉多個關聯資源,像是交易所的訂單、持倉、資金、實時行情全部互相影響,或是 POS 系統的購物車、折扣、庫存、結帳流程,Redux 的集中式 store 加上 DevTools 才真正體現價值。每一個 action 都有完整記錄,新加入的工程師打開 DevTools 就能看懂整個狀態流,這在大團隊協作上省了很多溝通成本。

參考資源