☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
react 中級

🏪 React 狀態管理:Redux Toolkit 與 Zustand

📌 為什麼需要全域狀態管理?

當應用變大,元件之間共享資料變得複雜。用 props 層層傳遞(prop drilling)會讓程式碼難以維護。

問題示意:
App → Layout → Sidebar → UserMenu → Avatar
                                      ↑
        使用者資料要從 App 傳 4 層才到 Avatar

💡 全域狀態管理讓任何元件都能直接存取共享資料,不用層層傳遞。


🔧 Redux Toolkit(RTK)

Redux Toolkit 是 Redux 的官方推薦工具包,大幅簡化了 Redux 的樣板程式碼。

npm install @reduxjs/toolkit react-redux

createSlice — 定義狀態和操作

// store/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    totalAmount: 0,
  },
  reducers: {
    // 每個 reducer 自動產生對應的 action
    addItem: (state, action) => {
      const existing = state.items.find(i => i.id === action.payload.id);
      if (existing) {
        existing.quantity += 1;       // RTK 用 Immer,可以直接修改
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
      state.totalAmount = state.items.reduce(
        (sum, item) => sum + item.price * item.quantity, 0
      );
    },
    removeItem: (state, action) => {
      state.items = state.items.filter(i => i.id !== action.payload);
      state.totalAmount = state.items.reduce(
        (sum, item) => sum + item.price * item.quantity, 0
      );
    },
    clearCart: (state) => {
      state.items = [];
      state.totalAmount = 0;
    },
  },
});

export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;

⚠️ RTK 使用 Immer:看起來像直接修改 state(state.items.push(...)), 但底層會產生新的不可變物件。這不是原生 JS 行為,是 RTK 的魔法。

configureStore — 建立 Store

// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer,      // 註冊 slice
  },
});

在元件中使用

// main.jsx — 用 Provider 包裝
import { Provider } from 'react-redux';
import { store } from './store';

createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App />
  </Provider>
);
// components/Cart.jsx — 讀取和操作 store
import { useSelector, useDispatch } from 'react-redux';
import { addItem, removeItem, clearCart } from '../store/cartSlice';

function Cart() {
  const { items, totalAmount } = useSelector(state => state.cart);
  const dispatch = useDispatch();

  return (
    <div>
      <h2>購物車({items.length} 件商品)</h2>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.name} x{item.quantity} — ${item.price * item.quantity}</span>
          <button onClick={() => dispatch(removeItem(item.id))}>移除</button>
        </div>
      ))}
      <p>總計:${totalAmount}</p>
      <button onClick={() => dispatch(clearCart())}>清空購物車</button>
    </div>
  );
}

🌊 RTK Query — 資料取得

RTK Query 自動處理快取、載入狀態、錯誤處理。

// store/api.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const productApi = createApi({
  reducerPath: 'productApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
  endpoints: (builder) => ({
    getProducts: builder.query({
      query: () => '/products',
    }),
    getProductById: builder.query({
      query: (id) => `/products/${id}`,
    }),
  }),
});

export const { useGetProductsQuery, useGetProductByIdQuery } = productApi;
// 使用自動產生的 Hook
function ProductList() {
  const { data: products, isLoading, error } = useGetProductsQuery();

  if (isLoading) return <p>載入中...</p>;
  if (error) return <p>錯誤:{error.message}</p>;

  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name} - ${p.price}</li>)}
    </ul>
  );
}

🐻 Zustand — 更輕量的替代方案

Zustand 比 Redux 更簡潔,適合中小型專案。

npm install zustand
// store/useCartStore.js
import { create } from 'zustand';

const useCartStore = create((set, get) => ({
  items: [],
  totalAmount: 0,

  addItem: (product) => set((state) => {
    const existing = state.items.find(i => i.id === product.id);
    const newItems = existing
      ? state.items.map(i =>
          i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
        )
      : [...state.items, { ...product, quantity: 1 }];

    return {
      items: newItems,
      totalAmount: newItems.reduce((s, i) => s + i.price * i.quantity, 0),
    };
  }),

  removeItem: (id) => set((state) => {
    const newItems = state.items.filter(i => i.id !== id);
    return {
      items: newItems,
      totalAmount: newItems.reduce((s, i) => s + i.price * i.quantity, 0),
    };
  }),

  clearCart: () => set({ items: [], totalAmount: 0 }),
}));

export default useCartStore;
// 使用 — 不需要 Provider!
function Cart() {
  const { items, totalAmount, removeItem, clearCart } = useCartStore();

  return (
    <div>
      <h2>購物車</h2>
      {items.map(item => (
        <div key={item.id}>
          {item.name} x{item.quantity}
          <button onClick={() => removeItem(item.id)}>移除</button>
        </div>
      ))}
      <p>總計:${totalAmount}</p>
      <button onClick={clearCart}>清空</button>
    </div>
  );
}

🆚 比較 Context vs Redux vs Zustand

特性 Context Redux Toolkit Zustand
設定複雜度
套件大小 0(內建) ~11KB ~1KB
效能 一般(全部重新渲染) 好(精確訂閱) 好(精確訂閱)
適合場景 主題、語言切換 大型應用、複雜邏輯 中小型應用
開發者工具 Redux DevTools Redux DevTools
非同步處理 自己處理 RTK Query / Thunk 直接在 store 寫

💡 選擇建議

  • 簡單共享(主題、語言)→ Context
  • 中小專案 → Zustand
  • 大型企業專案 → Redux Toolkit

✅ 本章重點

工具 核心概念
Redux Toolkit createSlice + configureStore + Immer
RTK Query 自動快取 + 載入狀態 + Hook
Zustand create() 一個函式搞定
選擇依據 專案規模和團隊偏好

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊