【TanStack Query + Zustand 教學】#01 為什麼需要分開管理 Server State 與 Client State?

測驗:為什麼需要分開管理 Server State 與 Client State

共 5 題,點選答案後會立即顯示結果

1. 下列哪個是 Client State 的典型範例?

  • A. 從 API 取得的使用者列表
  • B. Modal 是否開啟的狀態
  • C. 後端回傳的購物車資料
  • D. 從資料庫取得的文章內容

2. Server State 相較於 Client State,有什麼本質上的差異?

  • A. Server State 不需要快取策略
  • B. Server State 只有你的程式碼會改變它
  • C. Server State 可能已經過期,你看到的不一定是最新的
  • D. Server State 關掉頁面就可以消失

3. 使用傳統 Redux 管理 Server State 時,常見的痛點不包含下列哪項?

  • A. 每個 API 都要寫 start/success/failure 三個 action
  • B. 快取邏輯散落各處,判斷容易出錯
  • C. 兩個元件同時 mount,會發兩次相同的 API 請求
  • D. 無法使用 TypeScript 進行型別檢查

4. TanStack Query 提供了哪些 Server State 管理功能?

  • A. 僅提供 loading 狀態管理
  • B. 自動快取、去重複請求、視窗 focus 時自動 refetch、錯誤重試
  • C. 僅提供錯誤自動重試功能
  • D. 只能手動管理 loading/error/success 狀態

5. 關於 Zustand 的特點,下列敘述何者正確?

  • A. 需要 Provider 包裝整個應用程式
  • B. Bundle size 約 50KB
  • C. API 簡潔,不需要 Redux 的樣板程式碼
  • D. 不支援 TypeScript

前言

當你的 React 應用程式開始變得複雜,你可能會發現一件事:不是所有的狀態都一樣。有些狀態來自後端 API,有些則是使用者在畫面上的操作。把這兩種狀態混在一起管理,往往會讓程式碼變得難以維護。

本篇文章會帶你理解 Server State 與 Client State 的本質差異,以及為什麼 TanStack Query + Zustand 這個組合能讓你的程式碼更清晰。

Server State vs Client State:兩種完全不同的東西

什麼是 Client State?

Client State 是只存在於前端的狀態,完全由使用者的操作控制。

常見範例:

  • Modal 是否開啟(isModalOpen
  • 目前選中的 Tab(activeTab
  • 表單輸入中的文字(formDraft
  • 深色/淺色主題切換(theme

這些狀態的特點:

  • 你擁有完全控制權:只有你的程式碼會改變它
  • 沒有同步問題:不需要擔心資料過期
  • 不需要快取:關掉頁面就可以消失

什麼是 Server State?

Server State 是存放在後端的資料,你的前端只是「借來用」。

常見範例:

  • 使用者列表(users
  • 文章內容(posts
  • 購物車資料(cart
  • 使用者個人資料(profile

這些狀態的特點:

  • 你沒有完全控制權:後端、其他使用者都可能改變它
  • 可能已經過期:你看到的可能不是最新的
  • 需要快取策略:重複請求同樣資料很浪費
  • 有非同步特性:loading、error、success 三種狀態

用一張表看清楚差異

特性 Client State Server State
資料來源 前端產生 後端 API
控制權 完全掌控 共享(後端、其他用戶)
生命週期 頁面存活期間 需要同步與快取
狀態複雜度 單純(有/沒有) 複雜(loading/error/stale/fresh)

傳統方案的痛點

假設你要用 Redux 來管理一個「取得使用者列表」的需求,你的程式碼可能長這樣:

// userSlice.js - 傳統 Redux 寫法

const initialState = {
  users: [],
  isLoading: false,
  error: null,
  lastFetched: null,
};

const userSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    fetchUsersStart(state) {
      state.isLoading = true;
      state.error = null;
    },
    fetchUsersSuccess(state, action) {
      state.users = action.payload;
      state.isLoading = false;
      state.lastFetched = Date.now();
    },
    fetchUsersFailure(state, action) {
      state.error = action.payload;
      state.isLoading = false;
    },
  },
});
Code language: JavaScript (javascript)
// 元件中使用
function UserList() {
  const dispatch = useDispatch();
  const { users, isLoading, error, lastFetched } = useSelector(state => state.users);

  useEffect(() => {
    // 手動判斷是否需要重新取得
    const isStale = !lastFetched || Date.now() - lastFetched > 5 * 60 * 1000;
    if (isStale) {
      dispatch(fetchUsersStart());
      fetch('/api/users')
        .then(res => res.json())
        .then(data => dispatch(fetchUsersSuccess(data)))
        .catch(err => dispatch(fetchUsersFailure(err.message)));
    }
  }, [dispatch, lastFetched]);

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

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
Code language: JavaScript (javascript)

這段程式碼有什麼問題?

  1. 樣板程式碼太多:每個 API 都要寫 start/success/failure 三個 action
  2. 快取邏輯散落各處lastFetched 的判斷容易出錯
  3. 重複請求問題:兩個元件同時 mount,會發兩次 API
  4. 沒有自動重新驗證:使用者切回頁面時,資料可能已過期
  5. 錯誤重試要自己寫:網路暫時斷線?你得自己處理

現代方案:各司其職

TanStack Query 專門處理 Server State

// 使用 TanStack Query
function UserList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
  });

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

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
Code language: JavaScript (javascript)

TanStack Query 幫你處理好:

  • 自動快取與去重複請求
  • 視窗重新 focus 時自動 refetch
  • 錯誤自動重試
  • loading/error/success 狀態管理
  • 背景更新(stale-while-revalidate)

Zustand 專門處理 Client State

// 使用 Zustand
import { create } from 'zustand';

const useUIStore = create((set) => ({
  isModalOpen: false,
  activeTab: 'home',
  openModal: () => set({ isModalOpen: true }),
  closeModal: () => set({ isModalOpen: false }),
  setActiveTab: (tab) => set({ activeTab: tab }),
}));
Code language: JavaScript (javascript)
// 元件中使用
function Header() {
  const { activeTab, setActiveTab } = useUIStore();

  return (
    <nav>
      <button
        onClick={() => setActiveTab('home')}
        className={activeTab === 'home' ? 'active' : ''}
      >
        首頁
      </button>
      <button
        onClick={() => setActiveTab('profile')}
        className={activeTab === 'profile' ? 'active' : ''}
      >
        個人資料
      </button>
    </nav>
  );
}
Code language: PHP (php)

Zustand 的優點:

  • 極簡 API,沒有 Redux 的樣板程式碼
  • 不需要 Provider 包裝
  • TypeScript 支援良好
  • Bundle size 極小(約 1KB)

為什麼這個組合比「全用一個」更好?

比起「全用 Redux」

面向 全用 Redux TanStack Query + Zustand
樣板程式碼 大量 極少
Server State 處理 手動處理快取、loading、error 自動處理
學習曲線 陡峭 平緩
Bundle size 較大 較小

比起「全用 Context」

面向 全用 Context TanStack Query + Zustand
效能 容易造成不必要的 re-render 精準更新
Server State 功能 完全沒有 完整支援
程式碼組織 容易混亂 職責分明

實際專案中的分工

你的 React 應用程式
├── Server StateTanStack Query 管理)
│   ├── 使用者資料 useQuery(['user', userId])
│   ├── 文章列表 useQuery(['posts'])
│   ├── 評論資料 useQuery(['comments', postId])
│   └── 購物車 useQuery(['cart'])
│
└── Client StateZustand 管理)
    ├── UI 狀態(modalsidebartab)
    ├── 表單草稿
    ├── 使用者偏好設定
    └── 暫存的過濾條件
Code language: CSS (css)

重點整理

  1. Server State 與 Client State 本質不同:前者有同步、快取、過期等議題,後者沒有
  2. 傳統方案的痛點:用 Redux 或 Context 管理 Server State,會產生大量樣板程式碼,且要手動處理快取與 loading 狀態
  3. 現代方案的分工
    • TanStack Query:專門處理 Server State,自動管理快取、refetch、retry
    • Zustand:專門處理 Client State,API 簡潔、效能好
  4. 組合使用的優勢:職責分明、程式碼更少、效能更好、維護更容易

下一篇預告

下一篇我們會實際動手,學習 TanStack Query 的核心 API:useQueryuseMutation,看看它是如何用最少的程式碼處理複雜的 Server State。

進階測驗:為什麼需要分開管理 Server State 與 Client State

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個電商網站,需要顯示購物車資料和一個「展開/收合側邊欄」的按鈕。你應該如何安排狀態管理? 情境題

  • A. 全部用 Redux 管理,統一放在 store 中
  • B. 全部用 React Context 管理,方便跨元件共享
  • C. 購物車資料用 TanStack Query,側邊欄狀態用 Zustand
  • D. 購物車資料用 Zustand,側邊欄狀態用 TanStack Query

2. 你的同事寫了以下程式碼來管理使用者列表,但發現兩個元件同時載入時會發出兩次相同的 API 請求。這段程式碼的問題是什麼? 錯誤診斷

// Redux slice const userSlice = createSlice({ name: ‘users’, initialState: { users: [], isLoading: false }, reducers: { fetchUsersStart(state) { state.isLoading = true; }, fetchUsersSuccess(state, action) { state.users = action.payload; state.isLoading = false; } } }); // 元件中 useEffect(() => { dispatch(fetchUsersStart()); fetch(‘/api/users’) .then(res => res.json()) .then(data => dispatch(fetchUsersSuccess(data))); }, []);
  • A. Redux 無法處理非同步操作
  • B. 傳統 Redux 沒有內建去重複請求的機制
  • C. useEffect 的 dependency array 寫錯了
  • D. fetchUsersStart 應該放在 fetchUsersSuccess 之後

3. 你需要實作一個功能:使用者切換回瀏覽器分頁時,自動重新取得最新的訂單資料。你會選擇哪個方案? 情境題

  • A. 使用 TanStack Query 的 useQuery,它內建視窗 focus 時自動 refetch
  • B. 使用 Zustand 並手動監聽 window focus 事件
  • C. 使用 Redux 並在每個元件的 useEffect 中加入 focus 監聽
  • D. 使用 React Context 配合 setInterval 定時更新

4. 同事抱怨說:「我用 Context 管理所有狀態,但不知道為什麼頁面好像很卡。」根據文章內容,最可能的原因是什麼? 錯誤診斷

  • A. Context 不支援非同步操作
  • B. Context 的 API 太複雜導致程式碼效率低
  • C. Context 容易造成不必要的 re-render,影響效能
  • D. Context 只能管理少量資料

5. 你在重構專案時,需要決定如何管理「使用者選擇的深色/淺色主題」和「從 API 取得的使用者個人資料」。根據文章的分類原則,這兩種資料分別屬於什麼類型? 情境題

  • A. 都是 Server State,因為都跟使用者相關
  • B. 主題是 Client State,個人資料是 Server State
  • C. 都是 Client State,因為都是使用者的偏好
  • D. 主題是 Server State,個人資料是 Client State

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *