🏪 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() 一個函式搞定 |
| 選擇依據 | 專案規模和團隊偏好 |