測驗:為什麼需要分開管理 Server State 與 Client State
共 5 題,點選答案後會立即顯示結果
1. 下列哪個是 Client State 的典型範例?
2. Server State 相較於 Client State,有什麼本質上的差異?
3. 使用傳統 Redux 管理 Server State 時,常見的痛點不包含下列哪項?
4. TanStack Query 提供了哪些 Server State 管理功能?
5. 關於 Zustand 的特點,下列敘述何者正確?
前言
當你的 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)這段程式碼有什麼問題?
- 樣板程式碼太多:每個 API 都要寫 start/success/failure 三個 action
- 快取邏輯散落各處:
lastFetched的判斷容易出錯 - 重複請求問題:兩個元件同時 mount,會發兩次 API
- 沒有自動重新驗證:使用者切回頁面時,資料可能已過期
- 錯誤重試要自己寫:網路暫時斷線?你得自己處理
現代方案:各司其職
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 State(TanStack Query 管理)
│ ├── 使用者資料 useQuery(['user', userId])
│ ├── 文章列表 useQuery(['posts'])
│ ├── 評論資料 useQuery(['comments', postId])
│ └── 購物車 useQuery(['cart'])
│
└── Client State(Zustand 管理)
├── UI 狀態(modal、sidebar、tab)
├── 表單草稿
├── 使用者偏好設定
└── 暫存的過濾條件
Code language: CSS (css)重點整理
- Server State 與 Client State 本質不同:前者有同步、快取、過期等議題,後者沒有
- 傳統方案的痛點:用 Redux 或 Context 管理 Server State,會產生大量樣板程式碼,且要手動處理快取與 loading 狀態
- 現代方案的分工:
- TanStack Query:專門處理 Server State,自動管理快取、refetch、retry
- Zustand:專門處理 Client State,API 簡潔、效能好
- 組合使用的優勢:職責分明、程式碼更少、效能更好、維護更容易
下一篇預告
下一篇我們會實際動手,學習 TanStack Query 的核心 API:useQuery 與 useMutation,看看它是如何用最少的程式碼處理複雜的 Server State。
進階測驗:為什麼需要分開管理 Server State 與 Client State
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發一個電商網站,需要顯示購物車資料和一個「展開/收合側邊欄」的按鈕。你應該如何安排狀態管理? 情境題
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)));
}, []);