状态管理的核心问题不是「用什么库」,而是「状态放在哪里」。 服务端状态、客户端全局状态、组件本地状态、URL 状态各有最佳归宿,选对位置比选对工具更重要。
一、状态分类
1.1 四种状态类型
状态 = 数据 + 变化规则
┌─────────────────────────────────────────┐
│ A. 服务端状态(Server State) │
│ 特征:远程、异步、不可预测 │
│ 例子:用户信息、订单列表、商品详情 │
│ 方案:TanStack Query / SWR / Apollo │
├─────────────────────────────────────────┤
│ B. 客户端全局状态(Client Global) │
│ 特征:本地、同步、跨组件共享 │
│ 例子:主题、登录态、侧边栏展开 │
│ 方案:Redux / Zustand / Pinia / Jotai │
├─────────────────────────────────────────┤
│ C. 组件本地状态(Component Local) │
│ 特征:局部、短暂、不共享 │
│ 例子:表单输入、开关状态、动画标志 │
│ 方案:useState / ref / reactive │
├─────────────────────────────────────────┤
│ D. URL 状态(URL State) │
│ 特征:可分享、可刷新保持、可后退 │
│ 例子:分页页码、筛选条件、搜索关键词 │
│ 方案:URL query params / Router state │
└─────────────────────────────────────────┘
二、服务端状态管理
2.1 TanStack Query(React/Vue/Svelte)
// React 示例
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 读取
function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
gcTime: 10 * 60 * 1000, // 缓存保留 10 分钟
retry: 3, // 失败重试 3 次
refetchOnWindowFocus: false, // 切换回页面不自动刷新
});
}
// 写入 + 乐观更新
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UserUpdate) =>
fetch(`/api/users/${data.id}`, { method: 'PATCH', body: JSON.stringify(data) }),
onMutate: async (newData) => {
// 乐观更新:先改 UI,后发请求
await queryClient.cancelQueries({ queryKey: ['user', newData.id] });
const prev = queryClient.getQueryData(['user', newData.id]);
queryClient.setQueryData(['user', newData.id], (old) => ({ ...old, ...newData }));
return { prev };
},
onError: (err, newData, context) => {
// 出错回滚
queryClient.setQueryData(['user', newData.id], context?.prev);
},
onSettled: (data, err, newData) => {
// 最终同步
queryClient.invalidateQueries({ queryKey: ['user', newData.id] });
}
});
}
核心能力:
- 自动缓存、去重、后台刷新
- 乐观更新、重试、分页、无限滚动
- DevTools 调试
- SSR 支持
2.2 SWR(React)
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
const fetcher = (url: string) => fetch(url).then(r => r.json());
function Profile() {
const { data, error, isLoading, mutate } = useSWR('/api/user', fetcher, {
refreshInterval: 30000, // 每 30s 自动刷新
revalidateOnFocus: true, // 聚焦时刷新
dedupingInterval: 2000, // 2s 内去重
});
if (isLoading) return <div>loading...</div>;
if (error) return <div>failed to load</div>;
return <div>hello {data.name}!</div>;
}
2.3 服务端状态选型
| 维度 | TanStack Query | SWR |
|---|---|---|
| 框架支持 | React/Vue/Svelte/ Solid | React |
| 乐观更新 | 内置完善 | 需手动 |
| 分页/无限滚动 | 内置 hooks | 需组合 |
| 开发体验 | 稍复杂但更强 | 极简 |
| 推荐 | ✅ 大型项目 | ✅ 小型/快速原型 |
三、React 客户端全局状态
3.1 Redux Toolkit(RTK)
适合:大型应用、严格的数据流、团队需要统一模式
// store.ts
import { configureStore, createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 异步 thunk
const fetchUser = createAsyncThunk('user/fetch', async (userId: string) => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
});
// Slice
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null },
reducers: {
logout: (state) => { state.data = null; }
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.loading = true; })
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
export const store = configureStore({
reducer: { user: userSlice.reducer }
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// hooks.ts — 类型安全封装
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
3.2 Zustand
适合:中小型应用、快速开发、不喜欢 Redux 样板代码
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface BearState {
bears: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
const useBearStore = create<BearState>()(
persist(
(set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
decrease: () => set((state) => ({ bears: state.bears - 1 })),
reset: () => set({ bears: 0 }),
}),
{ name: 'bear-storage' } // localStorage 持久化
)
);
// 使用(无 Provider!)
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears} bears</h1>;
}
Zustand vs Redux:
| 维度 | Zustand | Redux |
|---|---|---|
| 学习成本 | 极低 | 中等 |
| 样板代码 | 几乎没有 | 较多 |
| DevTools | 支持 | 原生强大 |
| 中间件 | 简洁 | 丰富(saga/thunk) |
| 时间旅行 | 需配置 | 原生 |
| 适用规模 | 小到中 | 中到大 |
3.3 Jotai(原子化状态)
适合:细粒度状态、派生状态复杂、喜欢函数式风格
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
// 基础原子
const countAtom = atom(0);
// 派生原子(只读)
const doubleCountAtom = atom((get) => get(countAtom) * 2);
// 可写派生原子
const incrementAtom = atom(null, (get, set, amount: number) => {
set(countAtom, (c) => c + amount);
});
// 异步原子
const userAtom = atom(async () => {
const res = await fetch('/api/user');
return res.json();
});
// 使用
function Counter() {
const [count, setCount] = useAtom(countAtom);
const double = useAtomValue(doubleCountAtom);
const increment = useSetAtom(incrementAtom);
return (
<div>
<p>{count} / {double}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => increment(5)}>+5</button>
</div>
);
}
3.4 Valtio(Mutable State / Proxy)
适合:喜欢直接修改对象、从 Vue/MobX 迁移
import { proxy, useSnapshot } from 'valtio';
const state = proxy({
user: { name: 'Alice', age: 30 },
todos: [] as { id: number; text: string; done: boolean }[],
});
function Profile() {
const snap = useSnapshot(state);
// 直接修改(自动触发重渲染)
const increaseAge = () => { state.user.age++; };
return (
<div>
<p>{snap.user.name} is {snap.user.age}</p>
<button onClick={increaseAge}>过生日</button>
</div>
);
}
四、Vue 客户端全局状态
4.1 Pinia(Vue 官方推荐)
// stores/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null);
const loading = ref(false);
// Getters(computed)
const isLoggedIn = computed(() => user.value !== null);
const displayName = computed(() => user.value?.name ?? 'Guest');
// Actions
async function login(credentials: Credentials) {
loading.value = true;
try {
user.value = await api.login(credentials);
} finally {
loading.value = false;
}
}
function logout() {
user.value = null;
}
return { user, loading, isLoggedIn, displayName, login, logout };
}, {
// 持久化(需 pinia-plugin-persistedstate)
persist: { paths: ['user'] }
});
<!-- Component.vue -->
<script setup>
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { isLoggedIn, displayName } = storeToRefs(userStore); // 保持响应式解构
const { login, logout } = userStore; // 方法直接解构
</script>
4.2 Pinia vs Vuex
| 维度 | Pinia | Vuex 4 |
|---|---|---|
| API 风格 | Composition API(setup) | Options API(mutations/actions) |
| TypeScript | 原生友好 | 需类型封装 |
| 模块 | 自动(每个 store 独立) | 需手动注册 |
| 体积 | 更小(~1KB) | 稍大 |
| DevTools | 支持 | 支持 |
| 推荐 | ✅ Vue 3 首选 | ⚠️ 维护模式 |
五、跨框架方案
5.1 信号(Signals)— 未来趋势
// Solid / Preact / Vue Vapor / Angular 都在拥抱 Signals
// 核心思想:细粒度响应,只有读取 Signal 的组件才更新
// Vue(Reactivity Vapor 模式预览)
import { ref, computed, effect } from '@vue/reactivity';
const count = ref(0);
const double = computed(() => count.value * 2);
effect(() => {
console.log(double.value); // 依赖追踪,自动重新执行
});
count.value++; // 触发 effect
5.2 XState(状态机)
import { createMachine, interpret } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
// 适合:复杂状态流转(订单状态、多步骤表单、关卡游戏)
// 不适合:简单计数器(过度设计)
六、选型决策树
状态类型是什么?
├── 服务端数据(API 返回)
│ └── 用 TanStack Query(React/Vue)或 SWR(React)
│
├── 客户端全局状态
│ ├── React 生态
│ │ ├── 大型团队 + 严格数据流 → Redux Toolkit
│ │ ├── 中小型 + 极简 API → Zustand ✅ 多数场景
│ │ ├── 细粒度 + 派生状态复杂 → Jotai
│ │ ├── 喜欢直接修改对象 → Valtio
│ │ └── 复杂状态机 → XState
│ │
│ └── Vue 生态
│ └── Pinia(唯一推荐) → Pinia ✅
│
├── 组件本地状态
│ ├── React → useState / useReducer / useRef
│ ├── Vue → ref / reactive / computed
│ └── 跨组件但范围有限 → Context(React)/ Provide(Vue)
│
└── URL 状态
└── 分页/筛选/搜索 → URL query params(可分享、可刷新)
临时弹窗状态 → 组件本地(不要污染 URL)
七、组合使用模式
// 推荐的多层状态管理架构
// 1. 服务端状态:TanStack Query(缓存、刷新、乐观更新)
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
// 2. 客户端全局状态:Zustand(主题、UI 状态、本地化)
const theme = useThemeStore((s) => s.theme);
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
// 3. 组件本地状态:useState(表单、开关、临时值)
const [formData, setFormData] = useState({ name: '', email: '' });
// 4. URL 状态:分页、筛选
const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get('page')) || 1;
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。