iT邦幫忙

2025 iThome 鐵人賽

DAY 0
0
Modern Web

前端工程師的 Modern Web 實踐之道系列 第 10

狀態管理選擇困難症:從 Redux 到 Zustand 的現代化方案

  • 分享至 

  • xImage
  •  

系列文章: 前端工程師的 Modern Web 實踐之道 - Day 10
預計閱讀時間: 10 分鐘
難度等級: ⭐⭐⭐☆☆

🎯 今日目標

在前一篇文章中,我們探討了現代化模組架構的設計原則。今天我們將深入討論前端開發中最讓人頭痛的問題之一:狀態管理。從早期的 jQuery 全域變數,到 Redux 的嚴格規範,再到現代化的 Zustand、Jotai 等輕量解決方案,狀態管理技術正在經歷一場革命。

為什麼要關注狀態管理?

  • 複雜度控制: 隨著應用規模增長,狀態管理直接影響專案維護成本
  • 開發效率: 合適的狀態管理方案能大幅提升開發和除錯效率
  • 使用者體驗: 狀態同步的準確性直接影響使用者體驗的一致性
  • 團隊協作: 統一的狀態管理模式降低團隊溝通成本

🔍 深度分析:狀態管理的技術本質

問題背景與現狀

還記得那些年我們用 jQuery 寫的「義大利麵條式」程式碼嗎?全域變數滿天飛,狀態散布在各個 DOM 元素的 data 屬性中,一個 bug 要追查半天。

// 傳統 jQuery 的狀態管理噩夢
var userInfo = {};
var cartItems = [];
var isLoading = false;

$('#login-btn').click(function() {
  isLoading = true;
  $('.spinner').show();
  // 誰知道這些狀態會在哪裡被修改?
});

現代前端開發中,我們面臨的狀態管理挑戰包括:

  1. 狀態同步問題: 多個組件間的狀態一致性
  2. 效能最佳化: 避免不必要的重新渲染
  3. 除錯困難: 狀態變更的追蹤和回溯
  4. 開發複雜度: 學習成本與維護成本的平衡

技術方案深入解析

Redux:嚴格但強大的狀態管理典範

Redux 基於 Flux 架構,強制執行單向資料流,確保狀態變更的可預測性。

// Redux 的經典實作
import { createStore, combineReducers } from 'redux';

// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const SET_USER = 'SET_USER';

// Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const setUser = (user) => ({ type: SET_USER, payload: user });

// Reducers
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case INCREMENT:
      return { ...state, count: state.count + 1 };
    case DECREMENT:
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

const userReducer = (state = { user: null }, action) => {
  switch (action.type) {
    case SET_USER:
      return { ...state, user: action.payload };
    default:
      return state;
  }
};

// Store
const rootReducer = combineReducers({
  counter: counterReducer,
  user: userReducer
});

const store = createStore(rootReducer);

Redux 的優勢:

  • 完全可預測的狀態變更
  • 強大的除錯工具 (Redux DevTools)
  • 豐富的中介層生態系統
  • 時光旅行除錯

Redux 的痛點:

  • 大量的樣板程式碼
  • 學習曲線陡峭
  • 簡單功能需要複雜設定

Redux Toolkit:現代化的 Redux 體驗

Redux Toolkit (RTK) 是 Redux 官方推薦的現代化寫法,大幅簡化了 Redux 的使用。

import { createSlice, configureStore } from '@reduxjs/toolkit';

// 使用 createSlice 簡化 Redux 邏輯
const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => {
      // 使用 Immer 讓我們可以直接「修改」狀態
      state.count += 1;
    },
    decrement: (state) => {
      state.count -= 1;
    },
    incrementByAmount: (state, action) => {
      state.count += action.payload;
    }
  }
});

const userSlice = createSlice({
  name: 'user',
  initialState: { user: null, loading: false, error: null },
  reducers: {
    setLoading: (state, action) => {
      state.loading = action.payload;
    },
    setUser: (state, action) => {
      state.user = action.payload;
      state.loading = false;
      state.error = null;
    },
    setError: (state, action) => {
      state.error = action.payload;
      state.loading = false;
    }
  }
});

// 自動生成 action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const { setLoading, setUser, setError } = userSlice.actions;

// 設定 store
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    user: userSlice.reducer
  },
  // 內建最佳實踐的中介層
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['persist/PERSIST']
      }
    })
});

Zustand:輕量級的現代化選擇

Zustand 提供了更簡潔的 API,同時保持強大的功能。

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

// 基礎 store
const useCounterStore = create((set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  // 可以直接在 store 中定義計算屬性
  doubleCount: () => get().count * 2
}));

// 結合中介層的進階使用
const useUserStore = create(
  devtools(
    persist(
      (set, get) => ({
        user: null,
        loading: false,
        error: null,

        // 非同步 action
        fetchUser: async (userId) => {
          set({ loading: true, error: null });
          try {
            const response = await fetch(`/api/users/${userId}`);
            const user = await response.json();
            set({ user, loading: false });
          } catch (error) {
            set({ error: error.message, loading: false });
          }
        },

        // 重設狀態
        logout: () => set({ user: null, error: null }),

        // 複雜的狀態計算
        isAuthenticated: () => !!get().user,
        userPermissions: () => get().user?.permissions || []
      }),
      {
        name: 'user-storage', // 持久化設定
        partialize: (state) => ({ user: state.user }) // 只持久化特定欄位
      }
    ),
    { name: 'user-store' } // DevTools 名稱
  )
);

// 在 React 組件中使用
function Counter() {
  const { count, increment, decrement, doubleCount } = useCounterStore();

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount()}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

// 選擇性訂閱,最佳化效能
function UserProfile() {
  // 只訂閱 user 和 loading,其他狀態變更不會觸發重新渲染
  const { user, loading, fetchUser } = useUserStore(
    (state) => ({
      user: state.user,
      loading: state.loading,
      fetchUser: state.fetchUser
    })
  );

  return (
    <div>
      {loading ? (
        <div>載入中...</div>
      ) : user ? (
        <div>歡迎,{user.name}!</div>
      ) : (
        <button onClick={() => fetchUser('123')}>載入使用者</button>
      )}
    </div>
  );
}

Jotai:原子化狀態管理的新思維

Jotai 採用原子化思維,將狀態拆分為最小單位,提供更細緻的狀態控制。

import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';

// 基礎原子
const countAtom = atom(0);
const userAtom = atom(null);
const loadingAtom = atom(false);

// 派生原子(計算屬性)
const doubleCountAtom = atom((get) => get(countAtom) * 2);
const isAuthenticatedAtom = atom((get) => !!get(userAtom));

// 寫入原子(類似 action)
const incrementAtom = atom(
  null, // 讀取函數為 null 表示這是純寫入原子
  (get, set) => set(countAtom, get(countAtom) + 1)
);

// 非同步原子
const fetchUserAtom = atom(
  null,
  async (get, set, userId) => {
    set(loadingAtom, true);
    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      set(userAtom, user);
    } catch (error) {
      console.error('Failed to fetch user:', error);
    } finally {
      set(loadingAtom, false);
    }
  }
);

// 在組件中使用
function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const doubleCount = useAtomValue(doubleCountAtom);
  const increment = useSetAtom(incrementAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

function UserProfile() {
  const user = useAtomValue(userAtom);
  const loading = useAtomValue(loadingAtom);
  const fetchUser = useSetAtom(fetchUserAtom);

  return (
    <div>
      {loading ? (
        <div>載入中...</div>
      ) : user ? (
        <div>歡迎,{user.name}!</div>
      ) : (
        <button onClick={() => fetchUser('123')}>載入使用者</button>
      )}
    </div>
  );
}

🎯 實戰演練:建立現代化狀態管理系統

場景:電商購物車系統

讓我們透過一個實際案例來比較不同的狀態管理方案。我們要建立一個具備以下功能的購物車系統:

  • 商品列表管理
  • 購物車操作(新增、移除、修改數量)
  • 使用者認證狀態
  • 訂單處理流程

Zustand 實作版本

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

// 商品管理 store
const useProductStore = create(
  devtools((set, get) => ({
    products: [],
    loading: false,
    error: null,

    fetchProducts: async () => {
      set({ loading: true, error: null });
      try {
        const response = await fetch('/api/products');
        const products = await response.json();
        set({ products, loading: false });
      } catch (error) {
        set({ error: error.message, loading: false });
      }
    },

    getProductById: (id) => {
      return get().products.find(product => product.id === id);
    }
  }), { name: 'product-store' })
);

// 購物車管理 store
const useCartStore = create(
  devtools(
    persist(
      (set, get) => ({
        items: [],

        addItem: (productId, quantity = 1) => {
          set((state) => {
            const existingItem = state.items.find(item => item.productId === productId);

            if (existingItem) {
              return {
                items: state.items.map(item =>
                  item.productId === productId
                    ? { ...item, quantity: item.quantity + quantity }
                    : item
                )
              };
            } else {
              return {
                items: [...state.items, { productId, quantity }]
              };
            }
          });
        },

        removeItem: (productId) => {
          set((state) => ({
            items: state.items.filter(item => item.productId !== productId)
          }));
        },

        updateQuantity: (productId, quantity) => {
          if (quantity <= 0) {
            get().removeItem(productId);
            return;
          }

          set((state) => ({
            items: state.items.map(item =>
              item.productId === productId
                ? { ...item, quantity }
                : item
            )
          }));
        },

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

        // 計算屬性
        getTotalItems: () => {
          return get().items.reduce((total, item) => total + item.quantity, 0);
        },

        getTotalPrice: () => {
          const { getProductById } = useProductStore.getState();
          return get().items.reduce((total, item) => {
            const product = getProductById(item.productId);
            return total + (product?.price || 0) * item.quantity;
          }, 0);
        }
      }),
      {
        name: 'cart-storage',
        partialize: (state) => ({ items: state.items })
      }
    ),
    { name: 'cart-store' }
  )
);

// 訂單管理 store
const useOrderStore = create(
  devtools((set, get) => ({
    currentOrder: null,
    orderHistory: [],
    processing: false,

    createOrder: async () => {
      const { items, clearCart } = useCartStore.getState();
      const { user } = useUserStore.getState();

      if (!user || items.length === 0) {
        throw new Error('無法建立訂單:使用者未登入或購物車為空');
      }

      set({ processing: true });

      try {
        const orderData = {
          userId: user.id,
          items,
          totalAmount: useCartStore.getState().getTotalPrice(),
          createdAt: new Date().toISOString()
        };

        const response = await fetch('/api/orders', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(orderData)
        });

        const order = await response.json();

        set((state) => ({
          currentOrder: order,
          orderHistory: [order, ...state.orderHistory],
          processing: false
        }));

        clearCart(); // 清空購物車
        return order;
      } catch (error) {
        set({ processing: false });
        throw error;
      }
    },

    fetchOrderHistory: async () => {
      const { user } = useUserStore.getState();
      if (!user) return;

      try {
        const response = await fetch(`/api/orders/user/${user.id}`);
        const orders = await response.json();
        set({ orderHistory: orders });
      } catch (error) {
        console.error('Failed to fetch order history:', error);
      }
    }
  }), { name: 'order-store' })
);

React 組件整合範例

import React, { useEffect } from 'react';

// 商品列表組件
function ProductList() {
  const { products, loading, error, fetchProducts } = useProductStore();
  const addItem = useCartStore(state => state.addItem);

  useEffect(() => {
    fetchProducts();
  }, [fetchProducts]);

  if (loading) return <div>載入商品中...</div>;
  if (error) return <div>錯誤:{error}</div>;

  return (
    <div className="product-grid">
      {products.map(product => (
        <div key={product.id} className="product-card">
          <h3>{product.name}</h3>
          <p>價格:${product.price}</p>
          <button onClick={() => addItem(product.id)}>
            加入購物車
          </button>
        </div>
      ))}
    </div>
  );
}

// 購物車組件
function Cart() {
  const { items, updateQuantity, removeItem, getTotalItems, getTotalPrice } = useCartStore();
  const { getProductById } = useProductStore();

  const cartItems = items.map(item => ({
    ...item,
    product: getProductById(item.productId)
  })).filter(item => item.product);

  return (
    <div className="cart">
      <h2>購物車 ({getTotalItems()} 件商品)</h2>

      {cartItems.length === 0 ? (
        <p>購物車是空的</p>
      ) : (
        <>
          {cartItems.map(item => (
            <div key={item.productId} className="cart-item">
              <span>{item.product.name}</span>
              <span>${item.product.price}</span>
              <input
                type="number"
                value={item.quantity}
                onChange={(e) => updateQuantity(item.productId, parseInt(e.target.value))}
                min="0"
              />
              <button onClick={() => removeItem(item.productId)}>
                移除
              </button>
            </div>
          ))}

          <div className="cart-total">
            總計:${getTotalPrice()}
          </div>

          <CheckoutButton />
        </>
      )}
    </div>
  );
}

// 結帳組件
function CheckoutButton() {
  const { createOrder, processing } = useOrderStore();
  const { user } = useUserStore();
  const { items } = useCartStore();

  const handleCheckout = async () => {
    if (!user) {
      alert('請先登入');
      return;
    }

    if (items.length === 0) {
      alert('購物車是空的');
      return;
    }

    try {
      const order = await createOrder();
      alert(`訂單建立成功!訂單編號:${order.id}`);
    } catch (error) {
      alert(`訂單建立失敗:${error.message}`);
    }
  };

  return (
    <button
      onClick={handleCheckout}
      disabled={processing || !user || items.length === 0}
      className="checkout-button"
    >
      {processing ? '處理中...' : '結帳'}
    </button>
  );
}

📊 效能最佳化與最佳實踐

狀態管理的效能考量

// 避免過度渲染的選擇器模式
function OptimizedProductList() {
  // ❌ 錯誤:會訂閱整個 store
  // const store = useProductStore();

  // ✅ 正確:只訂閱需要的狀態
  const products = useProductStore(state => state.products);
  const loading = useProductStore(state => state.loading);
  const fetchProducts = useProductStore(state => state.fetchProducts);

  // 或者使用 shallow 比較
  const { products, loading, fetchProducts } = useProductStore(
    state => ({
      products: state.products,
      loading: state.loading,
      fetchProducts: state.fetchProducts
    }),
    shallow // 需要 import { shallow } from 'zustand/shallow'
  );

  // 組件實作...
}

// 記憶化選擇器
const useCartSummary = () => {
  return useCartStore(
    useCallback((state) => ({
      totalItems: state.getTotalItems(),
      totalPrice: state.getTotalPrice(),
      itemCount: state.items.length
    }), []),
    shallow
  );
};

大型應用的狀態切片策略

// 使用切片模式組織大型應用狀態
const createAuthSlice = (set, get) => ({
  user: null,
  token: null,
  loading: false,

  login: async (credentials) => {
    set({ loading: true });
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials)
      });
      const { user, token } = await response.json();
      set({ user, token, loading: false });
    } catch (error) {
      set({ loading: false });
      throw error;
    }
  },

  logout: () => set({ user: null, token: null })
});

const createCartSlice = (set, get) => ({
  items: [],
  addItem: (productId, quantity) => { /* ... */ },
  removeItem: (productId) => { /* ... */ }
});

const createUISlice = (set, get) => ({
  sidebarOpen: false,
  theme: 'light',
  notifications: [],

  toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })),
  setTheme: (theme) => set({ theme }),
  addNotification: (notification) => set(state => ({
    notifications: [...state.notifications, { ...notification, id: Date.now() }]
  }))
});

// 組合所有切片
const useAppStore = create()(
  devtools(
    persist(
      (...args) => ({
        ...createAuthSlice(...args),
        ...createCartSlice(...args),
        ...createUISlice(...args)
      }),
      {
        name: 'app-storage',
        partialize: (state) => ({
          user: state.user,
          token: state.token,
          items: state.items,
          theme: state.theme
        })
      }
    )
  )
);

📋 本日重點回顧

  1. 技術演進理解: 從 Redux 的嚴格規範到現代化輕量方案的發展脈絡
  2. 方案選擇策略: 根據專案規模、團隊經驗、維護成本選擇合適的狀態管理方案
  3. 效能最佳化: 透過選擇器模式和狀態切片避免不必要的重新渲染

🎯 最佳實踐建議

  • 小型專案: 使用 React 內建的 useState/useContext 或 Zustand
  • 中型專案: Zustand 配合中介層,或者 Redux Toolkit
  • 大型專案: Redux Toolkit 配合完整的工程化工具鏈
  • 高度模組化需求: 考慮 Jotai 的原子化方案
  • 避免過度設計: 不要為了使用新技術而強行導入複雜的狀態管理
  • 避免狀態碎片化: 相關的狀態應該組織在一起,避免過度原子化

🤔 延伸思考

  1. 如何在現有專案中漸進式地從 Redux 遷移到 Zustand?
  2. 狀態管理和服務端狀態(如 React Query)的職責邊界在哪裡?
  3. 在微前端架構中,如何處理跨應用的狀態共享?

上一篇
模組化架構:如何組織大型前端應用的程式碼結構
下一篇
RESTful 到 GraphQL 的實踐經驗
系列文
前端工程師的 Modern Web 實踐之道13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言