iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Modern Web

JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天系列 第 14

離開 JS 初階工程師新手村的 Day 14|Redux 與 Zustand:不同魔法學派的選擇

  • 分享至 

  • xImage
  •  

比較

花了一些篇幅分別看 Context API 與 Zustand 的用法,接下來讓我們模擬一個實際的例子來理解這兩種加上 Redux 的差異。假設我們要實現一個使用者管理系統,包含登入狀態、主題切換和通知管理。

Context API

Context API 的設計哲學可以用「最小化依賴,最大化內聚」來概括。好處是大多數狀態共享的需求都可以用 React 自身的能力來解決,不需要引入額外的複雜性。

https://ithelp.ithome.com.tw/upload/images/20250925/20168365sVBhjTAJNu.png

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

Zustand 將狀態管理透過一個 store 集中管理,直觀易用、同時具備處理複雜場景的能力。

https://ithelp.ithome.com.tw/upload/images/20250925/20168365muMDDXclSD.png

與前者相比之下,無需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 Toolkit

Redux 利用嚴格的單向資料流不可變更新,確保應用的狀態變化完全可控、可追蹤、可調試。

https://ithelp.ithome.com.tw/upload/images/20250925/20168365VVTYIJYhj5.png

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 API:內建的選擇

Context 最一開始是為了解決 prop drilling 問題而出現的 hook,因此仍適用於這個情況。另外也適合小到中型的應用程式,因為當需要追蹤的狀態變多,首先 Provider 會隨之增加,雖然這邊帶來的問題並不大,但一層層終究可能帶來 Provider hell。

一旦應用程式 (或是團隊) 的規模變大或是有需要常常更新的狀態,就該考慮使用 Zustand 或 Redux,尤其 Zustand 的學習曲線低,很好上手。加上先前提到的,只要其中一個 state 更新,整個 Context 都會更新連動 rerender 相關的所有 Component,因此會有效能浪費的問題,比較適用於狀態更新頻率低,比如說全域的設定。

Zustand

像是一般常見的電商,這種中大型的應用,可能會有跨 Component 的狀態,在 Store 統一管理同步與非同步的狀態,真的很方便,還能同時兼顧開發的效率與程式碼的簡潔性。我自己平常主要就是使用 Zustand,搭配一些 Context 設定主題或是通知 (符合更新頻率低、跨域設定的特性)。

沒錯,這些工具是可以混著用的。

https://ithelp.ithome.com.tw/upload/images/20250925/20168365JK76NqIefA.png

Redux

雖然這個我自己沒有用過,查了一下發現他的學習曲線高一些且使用上較複雜,但因為非常的穩定,很適合處理複雜的 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 提供了最完整和成熟的解決方案,適合那些需要嚴格狀態管理和強大調試能力的大型應用。它的價值在於「可預測性勝過一切」的架構理念。


上一篇
離開 JS 初階工程師新手村的 Day 13|從 Context 到 Zustand:更好用的狀態商店
系列文
JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言