状态管理方案对比
状态管理方案对比
什么是状态管理?为什么需要关注?
在前端应用中,状态(State)是指驱动 UI 渲染的数据。一个简单的计数器只需要 useState,但当应用规模增长后,多个组件需要共享用户信息、主题配置、购物车数据、表单状态等。如果没有统一的状态管理方案,开发者将面临 prop drilling(逐层透传 props)、状态不一致、难以调试 等问题。
状态管理方案从早期的 Redux、Vuex 演进到如今的 Pinia、Zustand、Jotai 等轻量方案。理解各方案的设计理念和适用场景,有助于为项目选择合适的工具,避免过度设计或设计不足。
状态的分类
在讨论状态管理方案之前,首先要明确状态的不同类型,因为不同类型的状态应该使用不同的管理方式:
| 状态类型 | 举例 | 推荐管理方式 |
|---|---|---|
| UI 状态 | 弹窗开关、加载中、hover | 组件本地 useState/ref |
| 服务端状态 | 用户列表、订单数据 | React Query / SWR |
| URL 状态 | 当前页码、筛选条件 | 路由参数 |
| 全局状态 | 主题、语言、认证 | Context / Zustand / Pinia |
| 表单状态 | 输入值、验证错误 | 表单库(React Hook Form) |
| 跨组件状态 | 选中行、拖拽数据 | 状态管理库 |
核心原则:全局状态尽量少,组件级状态优先,服务端状态交给专门库管理。
主流方案详解
1. Redux Toolkit — React 生态标准
Redux 是 React 生态中最成熟的状态管理方案,Redux Toolkit(RTK)是其官方推荐的简化封装。它采用函数式单向数据流:dispatch(action) → reducer → new state → re-render。
// store/userSlice.ts — 定义 Slice
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
// 异步 Thunk — 获取用户信息
export const fetchUser = createAsyncThunk(
'user/fetch',
async (userId: string, { rejectWithValue }) => {
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('请求失败');
return await res.json();
} catch (e) {
return rejectWithValue((e as Error).message);
}
}
);
interface UserState {
info: { id: string; name: string; role: string } | null;
loading: boolean;
error: string | null;
}
const userSlice = createSlice({
name: 'user',
initialState: { info: null, loading: false, error: null } as UserState,
reducers: {
clearUser: (state) => { state.info = null; },
updateName: (state, action: PayloadAction<string>) => {
if (state.info) state.info.name = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.loading = true; })
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.info = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
},
});
export const { clearUser, updateName } = userSlice.actions;
export default userSlice.reducer;// store/index.ts — 配置 Store
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
user: userReducer,
counter: counterReducer,
},
middleware: (getDefault) =>
getDefault().concat(/* 自定义中间件 */),
});
// TypeScript 类型推导
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// 类型安全的 Hook
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;// 组件中使用
function UserProfile() {
const dispatch = useAppDispatch();
const { info, loading, error } = useAppSelector((state) => state.user);
useEffect(() => { dispatch(fetchUser('123')); }, [dispatch]);
if (loading) return <Spinner />;
if (error) return <Error msg={error} />;
return (
<div>
<h1>{info?.name}</h1>
<button onClick={() => dispatch(updateName('新名称'))}>修改名称</button>
</div>
);
}Redux Toolkit 优势:
- Immer 内置,可以直接修改 state(
state.info.name = 'xxx') createAsyncThunk统一处理异步逻辑的 loading/error- RTK Query 内置数据请求和缓存管理
- DevTools 支持时间旅行调试
2. Zustand — 轻量级 React 状态管理
Zustand 以极简 API 和极小体积著称(~1KB),不需要 Provider 包裹,使用 Hook 直接消费状态。
// stores/userStore.ts
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
interface User {
id: string;
name: string;
role: 'admin' | 'user';
}
interface UserStore {
user: User | null;
token: string | null;
// Actions
login: (username: string, password: string) => Promise<void>;
logout: () => void;
updateUser: (data: Partial<User>) => void;
}
export const useUserStore = create<UserStore>()(
devtools(
persist(
(set) => ({
user: null,
token: null,
login: async (username, password) => {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
const data = await res.json();
set({ user: data.user, token: data.token });
},
logout: () => set({ user: null, token: null }),
updateUser: (data) =>
set((state) => ({
user: state.user ? { ...state.user, ...data } : null,
})),
}),
{
name: 'user-storage', // localStorage key
partialize: (state) => ({ token: state.token }), // 只持久化 token
}
),
{ name: 'UserStore' } // DevTools 名称
)
);// 使用 — 无需 Provider
function Navbar() {
// 只订阅 user,token 变化不会触发重渲染
const user = useUserStore((s) => s.user);
const logout = useUserStore((s) => s.logout);
if (!user) return <LoginButton />;
return (
<nav>
<span>欢迎, {user.name}</span>
<button onClick={logout}>退出</button>
</nav>
);
}
// 跨组件通信示例 — 不在同一组件树
function ComponentA() {
const updateUser = useUserStore((s) => s.updateUser);
return <button onClick={() => updateUser({ name: 'Updated' })}>更新</button>;
}
function ComponentB() {
const user = useUserStore((s) => s.user);
return <span>{user?.name}</span>; // 自动响应 ComponentA 的更新
}Zustand 的 Selector 优化:
// Bad — 每次渲染创建新的 selector,导致无限重渲染
const data = useStore((state) => {
return { name: state.user.name, role: state.user.role }; // 每次返回新对象
});
// Good — 使用 shallow 比较或拆分 selector
import { shallow } from 'zustand/shallow';
const { name, role } = useStore(
(state) => ({ name: state.user.name, role: state.user.role }),
shallow
);
// 更好的方式 — 拆分为独立的 selector
const name = useStore((s) => s.user.name);
const role = useStore((s) => s.user.role);3. Pinia — Vue3 官方推荐
Pinia 是 Vue3 的官方状态管理方案,完全支持 TypeScript,采用组合式 API 风格。
// stores/deviceStore.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { Device } from '@/types';
export const useDeviceStore = defineStore('device', () => {
// State — 使用 ref
const devices = ref<Device[]>([]);
const selectedId = ref<string | null>(null);
const loading = ref(false);
const filter = ref({ keyword: '', status: '' as string });
// Getters — 使用 computed
const onlineDevices = computed(() =>
devices.value.filter((d) => d.status === 'online')
);
const selectedDevice = computed(() =>
devices.value.find((d) => d.id === selectedId.value) ?? null
);
const filteredDevices = computed(() => {
let result = devices.value;
if (filter.value.keyword) {
const kw = filter.value.keyword.toLowerCase();
result = result.filter((d) => d.name.toLowerCase().includes(kw));
}
if (filter.value.status) {
result = result.filter((d) => d.status === filter.value.status);
}
return result;
});
// Actions — 普通函数
async function fetchDevices() {
loading.value = true;
try {
const res = await fetch('/api/devices');
devices.value = await res.json();
} finally {
loading.value = false;
}
}
function selectDevice(id: string) {
selectedId.value = id;
}
function setFilter(newFilter: Partial<typeof filter.value>) {
filter.value = { ...filter.value, ...newFilter };
}
// $reset 由 Pinia 自动生成(Option Store 风格)
// 组合式风格需要手动实现
function $reset() {
devices.value = [];
selectedId.value = null;
filter.value = { keyword: '', status: '' };
}
return {
// State
devices,
selectedId,
loading,
filter,
// Getters
onlineDevices,
selectedDevice,
filteredDevices,
// Actions
fetchDevices,
selectDevice,
setFilter,
$reset,
};
});<!-- DeviceList.vue — 组件中使用 -->
<script setup>
import { useDeviceStore } from '@/stores/deviceStore';
import { storeToRefs } from 'pinia';
const store = useDeviceStore();
// storeToRefs 解构保持响应式(仅对 state 和 getters 有效)
const { filteredDevices, loading, selectedId } = storeToRefs(store);
// Actions 直接解构(不需要响应式)
const { fetchDevices, selectDevice, setFilter } = store;
</script>
<template>
<div>
<input v-model="filter.keyword" @input="setFilter({ keyword: filter.keyword })" />
<button @click="fetchDevices" :disabled="loading">刷新</button>
<ul v-if="!loading">
<li
v-for="device in filteredDevices"
:key="device.id"
:class="{ active: device.id === selectedId }"
@click="selectDevice(device.id)"
>
{{ device.name }} — {{ device.status }}
</li>
</ul>
</div>
</template>4. Jotai/Recoil — 原子化状态管理
原子化状态管理与传统的单一 Store 不同,它将状态拆分为独立的原子(Atom),组件只订阅自己需要的原子,从而精确控制重渲染范围。
// atoms.ts — 定义原子
import { atom, selector } from 'jotai';
import { atomWithQuery } from 'jotai-tanstack-query';
// 原始原子
export const countAtom = atom(0);
export const keywordAtom = atom('');
export const themeAtom = atom<'light' | 'dark'>('light');
// 派生原子(只读)
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
// 异步原子
export const usersAtom = atomWithQuery(() => ({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('/api/users');
return res.json();
},
}));
// 可写派生原子
export const filteredUsersAtom = atom(
(get) => {
const users = get(usersAtom).data ?? [];
const keyword = get(keywordAtom).toLowerCase();
return keyword ? users.filter((u: any) => u.name.includes(keyword)) : users;
}
);// 组件中使用 — 无需 Provider
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { countAtom, doubleCountAtom, keywordAtom } from './atoms';
function Counter() {
// 读写原子
const [count, setCount] = useAtom(countAtom);
// 只读原子
const double = useAtomValue(doubleCountAtom);
// 只写原子
const setKeyword = useSetAtom(keywordAtom);
return (
<div>
<span>Count: {count}, Double: {double}</span>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<input onChange={(e) => setKeyword(e.target.value)} />
</div>
);
}
// 注意:keyword 变化不会触发 Counter 重渲染
// 因为 Counter 没有订阅 keywordAtom 的值方案选型决策
按项目规模选择
项目规模 & 方案选择:
- 小型项目 (< 10 页面) → Zustand / Pinia / 组件状态
- 中型项目 (10-50 页面) → Zustand / Pinia / Redux Toolkit
- 大型项目 (> 50 页面) → Redux Toolkit (团队规范)
- 高频更新 → Zustand (selector 优化) / Jotai (原子化)按框架选择
框架匹配:
- Vue3 → Pinia (官方推荐,完美集成)
- React → Zustand (轻量) / Redux Toolkit (标准)
- React + 微前端 → Jotai (原子化无 Provider)
- 跨框架 → Jotai (支持 React 和 Vue)核心维度对比
| 维度 | Redux Toolkit | Zustand | Pinia | Jotai |
|---|---|---|---|---|
| 体积 | ~11KB | ~1KB | ~1KB | ~2.5KB |
| 学习曲线 | 陡峭 | 平缓 | 平缓 | 中等 |
| DevTools | 完善 | 基础 | 完善 | 基础 |
| TypeScript | 良好 | 良好 | 优秀 | 良好 |
| 中间件 | 丰富 | 可选 | 内置 | 插件 |
| Provider | 需要 | 不需要 | 不需要 | 不需要 |
| 适用框架 | React | React | Vue | React |
服务端状态管理
客户端状态和服务端状态是两种不同的关注点。服务端状态具有以下特点:数据来源是异步的、可能过期、需要缓存策略、需要乐观更新等。
// React Query 管理服务端状态
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5分钟内认为数据新鲜
gcTime: 30 * 60 * 1000, // 30分钟后垃圾回收
});
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => fetch('/api/users', {
method: 'POST', body: JSON.stringify(newUser),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); // 自动刷新
},
});
}
// Vue3 — VueQuery
import { useQuery } from '@tanstack/vue-query';
const { data, isLoading } = useQuery({
queryKey: ['devices'],
queryFn: () => fetch('/api/devices').then(r => r.json()),
});最佳实践:服务端状态用 React Query/SWR,客户端状态用 Zustand/Pinia,不要把两者混在一起。
常见陷阱与排查
陷阱 1:Selector 返回新对象导致无限重渲染
// Bad — 每次渲染创建新对象
const user = useStore((state) => ({ name: state.name, age: state.age }));
// Good — 使用 shallow 或拆分
import { shallow } from 'zustand/shallow';
const { name, age } = useStore((state) => ({ name: state.name, age: state.age }), shallow);陷阱 2:把所有状态都放进全局 Store
错误做法:
- 弹窗开关 (isOpen)
- 表单输入值 (inputValue)
- 列表 hover 状态
正确做法:
- 只有需要跨组件共享的状态才放入全局 Store
- UI 状态留在组件内部
- 表单状态使用表单库管理陷阱 3:Context value 每次渲染创建新对象
// Bad — value 每次都是新对象,所有消费者重渲染
<ThemeContext.Provider value={{ mode, toggle, colors }}>
{children}
</ThemeContext.Provider>
// Good — useMemo 稳定引用
const value = useMemo(() => ({ mode, toggle, colors }), [mode, colors]);
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>排错清单
- 检查 selector 是否返回稳定引用(使用
shallow或拆分) - 用 DevTools 检查状态变更历史,确认变更来源
- 确认 Store 更新是否触发了不必要的重渲染(React Profiler)
- 检查是否把服务端状态和客户端状态混在一起
性能优化策略
1. 选择性订阅
// Zustand — 只订阅需要的字段
const count = useStore((s) => s.count); // 只有 count 变化才重渲染
// Redux — 使用 reselect 创建 memoized selector
import { createSelector } from '@reduxjs/toolkit';
const selectVisibleUsers = createSelector(
[(state) => state.users.list, (state) => state.users.filter],
(list, filter) => list.filter(u => u.name.includes(filter.keyword))
);2. 状态标准化
// Bad — 嵌套结构
{ users: [{ id: 1, name: 'A', posts: [{ id: 1, title: 'P1' }] }] }
// Good — 扁平化
{
users: { byId: { 1: { id: 1, name: 'A', postIds: [1] } }, allIds: [1] },
posts: { byId: { 1: { id: 1, title: 'P1', userId: 1 } }, allIds: [1] },
}3. 拆分 Store
// Redux — 多 Slice 拆分
const store = configureStore({
reducer: { user: userSlice, devices: deviceSlice, ui: uiSlice },
});
// Zustand — 独立 Store
const useUserStore = create(...);
const useDeviceStore = create(...);
const useUIStore = create(...);最佳实践总结
- 新项目优先选择:Vue3 用 Pinia,React 小项目用 Zustand,React 大团队用 Redux Toolkit
- 状态分层管理:服务端状态(React Query)+ 客户端全局状态(Zustand/Pinia)+ 组件本地状态
- 选择性订阅:始终使用 selector 精确订阅需要的字段
- 避免过度设计:不要在项目初期就引入复杂的状态管理方案
- 编写测试:为 Store 的核心逻辑编写单元测试
- DevTools 集成:开发环境始终开启 DevTools 方便调试
