React 状态管理是前端开发中争议最多、选择最丰富的领域。从内置的 useState 到遍布社区的几十个状态库,选择困难的根本原因是状态本身具有多种形态,不同形态需要不同的管理方案。本文构建完整的 React 状态管理知识体系,从底层原理到生产实践,按项目规模给出明确的选型路径。
一、状态分类:先分清你要管什么
所有状态混乱的根源是不做分类就直接选型。React 应用中的状态分为四类:
| 状态类型 | 示例 | 特征 | 管理方案 |
|---|---|---|---|
| 本地状态 | 输入框值、模态框开关 | 单个组件内使用 | useState / useReducer |
| 共享状态 | 用户信息、主题色 | 多个组件需要读写 | Zustand / Jotai / Context |
| 服务端状态 | API 返回的数据、缓存 | 来源于服务器,需同步 | TanStack Query / SWR |
| URL 状态 | 当前页码、筛选条件 | 反映在用户地址栏 | React Router + URL 参数 |
核心原则:不要用全局状态库管理服务器直接提供的数据,也不要用本地
useState管理跨组件的共享状态。选错层级是小项目变成状态泥沼的根本原因。
1.1 本地状态:useState vs useReducer
// useState:简单值或对象
const [count, setCount] = useState(0);
// useReducer:复杂状态转换逻辑
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch({ type: 'increment', payload: 5 }) —— 类似 Redux 但局限在单个组件
useReducer 适用条件:
- 状态修改逻辑复杂(超过 3 种不同的修改方式)
- 下一种状态依赖前一种状态(级联修改)
- 需要集中测试状态转换逻辑(reducer 是纯函数,可独立单元测试)
1.2 Context 的真相:它不是状态管理工具
React Context 在官方文档中的定位是依赖注入机制,不是状态管理方案。它的本质是组件树级别的广播系统。
Context 的重渲染陷阱:
// ❌ 反模式:把整个 store 放进一个 Context
function App() {
const [state, setState] = useState({ user, theme, sidebar, notifications });
return (
<AppContext.Provider value={{ state, setState }}>
<Layout /> {/* theme 变了,整个 Layout 会重渲染 */}
</AppContext.Provider>
);
}
Context 的问题在于:当 Provider 的 value 变化时,所有消费该 Context 的子组件都会重新渲染,无论它们实际依赖 value 中的哪个字段。
正确的 Context 用法:将 Context 拆分为垂直领域,每个 Context 只提供一类数据。
// ✅ 垂直拆分 Context,减少重渲染范围
function App() {
const user = useUserState();
const theme = useThemeState();
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Context 适用场景总结:
- ✅ 配置/主题等低频率变更的数据(几个月变一次)
- ✅ 依赖注入(如依赖倒置、测试 mock)
- ❌ 高频变更的状态(表格数据、表单字段)
- ❌ 大量状态字段(Context 的重渲染代价太高)
二、选型决策树:按项目规模匹配方案
项目规模 → 状态特征 → 推荐方案
[微项目] < 10 个共享状态
└── useState / useReducer 传递 + 垂直拆分 Context
[小项目] 10-30 个共享状态
└── Zustand(极简,零样板代码)
[中项目] 30-60 个共享状态
└── Zustand(模块化 slice)或 Jotai(原子化,按需订阅)
[大项目] 60+ 状态,多人协作
└── Redux Toolkit(强规范、DevTools、时间旅行)+
TanStack Query(服务端状态分离)
[服务端数据为主]
└── TanStack Query(缓存、重取、乐观更新、去重请求)+
Zustand(仅保留纯客户端状态)
三、Zustand:2025 年最推荐的入门方案
Zustand(德语“状态”)的设计理念是最少的 API,最强的能力。它不需要 Provider 包裹,没有样板代码,但支持状态派生、中间件、持久化等企业级特性。
3.1 基础用法
import { create } from 'zustand';
interface BearStore {
bears: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
const useBearStore = create<BearStore>((set, get) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
decrease: () => set((state) => ({ bears: Math.max(0, state.bears - 1) })),
reset: () => set({ bears: 0 }),
}));
// 组件中使用
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears} bears</h1>; // 只有 bears 变化时才重渲染
}
关键特性:useBearStore(selector) 支持按字段订阅。上面的组件只在 bears 变化时重渲染,increase 方法变化不会触发它。
3.2 模块化 Slice 模式(中项目必用)
import { create } from 'zustand';
// 用户 slice
const createUserSlice = (set: any, get: any) => ({
user: null as User | null,
isLoggedIn: false,
login: (user: User) => set({ user, isLoggedIn: true }),
logout: () => set({ user: null, isLoggedIn: false }),
});
// 购物车 slice
const createCartSlice = (set: any, get: any) => ({
items: [] as CartItem[],
addItem: (item: CartItem) => {
const { items } = get();
set({ items: [...items, item] });
},
total: () => {
const { items } = get();
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
});
// 组合 store
const useStore = create((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}));
3.3 多 Store 模式 vs 单 Store 模式
| 模式 | 适用 | 代码组织 |
|---|---|---|
| 单 Store(Slices) | < 30 个状态 | 所有 slice 在一个文件或文件夹下 |
| 多 Store | > 30 个状态,或状态间无关联 | useUserStore、useCartStore 等独立 |
| 混合模式 | 核心状态单 Store,独立模块多 Store | 主业务用单 Store,IM/通知等用独立 Store |
3.4 持久化与中间件
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
const useUserStore = create(
persist<UserState>(
(set) => ({
theme: 'dark',
sidebarOpen: true,
setTheme: (theme: string) => set({ theme }),
}),
{
name: 'user-preferences',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ theme: state.theme }), // 只持久化 theme
}
)
);
常用中间件:
persist:localStorage/sessionStorage 持久化devtools:Redux DevTools 集成immer:不可变数据的突变式写法subscribeWithSelector:精确订阅变化
四、Jotai:原子化状态与细粒度订阅
Jotai 由 react-spring 作者 Daishi Kato 开发,采用**原子(atom)**作为状态的基本单位。每个 atom 是独立的状态碎片,组件只订阅它需要的 atoms。
4.1 核心概念
import { atom, useAtom } from 'jotai';
// Primitive atoms:基础原子
const countAtom = atom(0);
// Derived atoms:派生原子(自动追踪依赖)
const doubleAtom = atom((get) => get(countAtom) * 2);
// Writable derived atoms
const tripleAtom = atom(
(get) => get(countAtom) * 3,
(get, set, newValue: number) => set(countAtom, newValue / 3)
);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [double] = useAtom(doubleAtom); // 只读
return (
<div>
<p>Count: {count}, Double: {double}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
}
4.2 Jotai vs Zustand:什么时候选 Jotai?
| 维度 | Zustand | Jotai |
|---|---|---|
| 心智模型 | 集中式 Store(类 Redux) | 分布式原子(类 Recoil) |
| API 数量 | 极少 | 原子 + hooks |
| TypeScript | 良好 | 极佳(原子自带类型) |
| 按需订阅 | ✅ Selector | ✅ 自动追踪依赖 |
| 派生状态 | 手动实现 | ✅ atom(get) 自动派生 |
| 异步状态 | ✅ 支持 | ✅ 支持 |
| 生态大小 | 大(中间件丰富) | 中等 |
推荐:如果你习惯于每个组件自己定义和管理 atoms,或者需要大量细粒度的状态组合和派生,选 Jotai。如果你偏好明确的 store 结构,选 Zustand。
4.3 Jotai 高级模式
// 异步原子(数据获取)
const postsAtom = atom(async () => {
const res = await fetch('/api/posts');
return res.json();
});
// 带有 write 的原子
const updatePostAtom = atom(
null,
async (get, set, { id, data }: { id: string; data: PostData }) => {
const res = await fetch(`/api/posts/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
const updated = await res.json();
set(postsAtom, (prev) => prev.map((p) => (p.id === id ? updated : p)));
}
);
// Provider scope(隔离状态,用于测试或 SSR)
function PostEditor({ id }: { id: string }) {
return (
<Provider>
<EditorContent id={id} />
</Provider>
);
}
五、Redux Toolkit:企业级首选
Redux 因样板代码过多而备受诟病,Redux Toolkit(RTK)是官方推荐的现代 Redux 写法,将 actions、reducers 和 middleware 压缩为简洁的 API。
5.1 RTK 基础:createSlice
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit';
// 异步 thunk
const fetchUser = createAsyncThunk('user/fetch', async (userId: string) => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
});
const userSlice = createSlice({
name: 'user',
initialState: { user: null, loading: false, error: null } as UserState,
reducers: {
clearUser: (state) => { state.user = null; },
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.loading = true; })
.addCase(fetchUser.fulfilled, (state, action) => {
state.user = action.payload;
state.loading = false;
})
.addCase(fetchUser.rejected, (state, action) => {
state.error = action.error.message;
state.loading = false;
});
},
});
const store = configureStore({
reducer: { user: userSlice.reducer },
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export { fetchUser, clearUser };
5.2 Redux 为什么还是企业级首选?
| 企业级需求 | Redux 的优势 |
|---|---|
| 可预测性 | 单一 Store,所有状态变更通过 action,完全可追踪 |
| 调试 | Redux DevTools 支持时间旅行、action replay、状态快照 |
| 测试 | Reducer 是纯函数,输入输出确定,易于单元测试 |
| 规范 | 强制的 action-reducer 结构,团队代码风格一致 |
| 中间件生态 | redux-saga、redux-observable 等处理复杂副作用 |
| 持久化 | redux-persist 成熟稳定 |
实际经验:如果你的团队超过 5 个前端开发者,或者状态逻辑涉及多个模块的联动操作,Redux Toolkit 的规范性带来的收益会超过 Zustand 的简洁性。
5.3 RTK Query:RTK 内的 TanStack Query 替代方案
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({ query: () => '/posts' }),
getPost: builder.query<Post, string>({ query: (id) => `/posts/${id}` }),
updatePost: builder.mutation<Post, { id: string; body: PostData }>({
query: ({ id, body }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body,
}),
}),
}),
});
export const { useGetPostsQuery, useGetPostQuery, useUpdatePostMutation } = api;
RTK Query 优势:与 Redux 深度集成,自动缓存、自动重取、乐观更新、去重,无需在 Redux + TanStack Query 之间切换。
六、TanStack Query:服务端状态的第一选择
服务端状态(Server State)管理是所有 React 应用中最大的误区。大多数开发者会把 API 返回的数据存进全局状态库(Redux/Zustand),然后手动写缓存、重取、错误处理,反而更复杂。
TanStack Query(前身为 React Query)是专门管理服务端状态的库,它把数据获取抽象为第一公民。
6.1 核心概念
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 查询
const { data, isLoading, error, isFetching } = useQuery({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
staleTime: 1000 * 60 * 5, // 5分钟内数据不会重新获取
gcTime: 1000 * 60 * 30, // 缓存保留30分钟(v5 参数名)
retry: 3, // 失败后重试3次
refetchOnWindowFocus: true, // 窗口聚焦时自动重取
});
queryKey 的设计原则:队列数组,第一个元素是领域,后续是参数。['posts']、['posts', 'draft']、['posts', { status: 'published', page: 1 }]。
6.2 乐观更新(Optimistic Updates)
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
// 1. 取消正在进行的重新获取
await queryClient.cancelQueries({ queryKey: ['posts', newPost.id] });
// 2. 保存之前的状态用于回滚
const previousPost = queryClient.getQueryData(['posts', newPost.id]);
// 3. 乐观更新 UI
queryClient.setQueryData(['posts', newPost.id], newPost);
return { previousPost };
},
onError: (err, newPost, context) => {
// 4. 发生错误时回滚
queryClient.setQueryData(['posts', newPost.id], context?.previousPost);
},
onSettled: (newPost) => {
// 5. 最终数据同步
queryClient.invalidateQueries({ queryKey: ['posts', newPost?.id] });
},
});
6.3 服务端状态 vs 客户端状态分离策略
// ✅ 推荐:分离策略
// 服务端状态:只用 TanStack Query
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
const { mutate: updateUser } = useMutation({ mutationFn: updateUserApi });
// 客户端状态:Zustand 或 Context
const theme = useStore((state) => state.theme);
const toggleSidebar = useStore((state) => state.toggleSidebar);
为什么分离?
- JavaScript(所有全局状态库)没有内置缓存失效策略,而 TanStack Query 的缓存失效是核心能力
- Redux/Zustand 管理服务端数据需要手动写大量样板代码(loading、error、重取、乐观更新)
- 分离后服务端状态的测试由 API 测试覆盖,客户端状态由单元测试覆盖,边界清晰
七、性能对比表
| 库 | 重渲染优化 | 内存占用 | 初始加载体积 | DevTools |
|---|---|---|---|---|
| Context | ❌ 无(全树重渲染) | 极小 | 内置 | React DevTools |
| Zustand | ✅ Selector 精确订阅 | 极小 | ~1KB | 可选 Redux DevTools |
| Jotai | ✅ 原子级订阅 | 极小 | ~3KB | 无(依赖 React DevTools) |
| Redux Toolkit | ✅ connect/useSelector | 中 | ~11KB(含 RTK Query) | Redux DevTools |
| TanStack Query | ✅ 缓存+去重 | 中 | ~12KB | React Query DevTools |
八、生产级代码规范
8.1 Store 文件组织
stores/
├── index.ts # store 导出
├── userStore.ts # 用户 slice(Zustand)
├── cartStore.ts # 购物车 slice
├── types.ts # 共享类型
└── middleware/ # 自定义中间件
8.2 状态命名约定
// ✅ 使用语义化命名,避免简写
interface UserState {
isLoading: boolean; // 异步操作进行中
isAuthenticated: boolean; // 认证状态
currentUser: User | null; // 当前用户信息
error: Error | null; // 错误对象
}
// ❌ 避免
interface BadState {
loading: boolean; // 语义弱
errorMessage: string; // 应该统一用 error: Error
data: any; // 具体类型不明
}
8.3 不要一切都全局化
// ❌ 反模式:把 Form 状态塞进全局 Store
const useBadStore = create(() => ({
loginForm: { username: '', password: '' }, // 只在 Login 页使用
}));
// ✅ 正确:Form 状态留在组件内
function LoginPage() {
const [form, setForm] = useState({ username: '', password: '' });
// 只在需要提交时才与全局状态交互
}
常见问题(FAQ)
Context、Zustand、Redux 怎么选?
- 小项目(< 5 状态):Zustand(简单,性能好)
- 中型项目(5-20 状态):Zustand(模块化 slice)或 Jotai(原子化)
- 大型团队(> 5 人):Redux Toolkit(规范强,DevTools 完善,action 可追溯)
- 服务端数据为主:TanStack Query + 少量 Zustand(客户端状态和 URL 状态)
为什么有了 TanStack Query 还需要状态管理库?
TanStack Query 管理服务端状态(API 数据),但客户端状态(主题、模态框、表单草稿、侧边栏展开)仍需要 Zustand/Redux。两者不是竞争关系,是互补关系。理想架构是:服务端状态交给 TanStack Query,只把纯客户端状态放进全局 Store。
Zustand 和 Jotai 到底哪个好?
两者都轻量、TypeScript 友好、按需订阅。Zustand 更像 Redux(集中式 store,直接修改);Jotai 更像 Recoil(分布式原子,组合派生)。如果团队已有 Redux 经验选 Zustand,如果喜欢函数式组合选 Jotai,实际性能差异可忽略。
Redux Toolkit 还需要中间件吗?
RTK 内置 Redux Thunk。如果需要复杂副作用流(如竞态处理、debounce、轮询),推荐 RTK Query 或 redux-saga。小型异步直接用 createAsyncThunk。
相关阅读
- React 详解 — React 核心概念与定位
- React Hooks 完全指南 — 状态管理的基础工具
- React + TypeScript 实战指南 — Zustand/Redux 的 TypeScript 模式
- React 性能优化深度指南 — 状态管理的渲染性能分析
- React Server Components 深度解析 — 服务端组件与客户端状态的关系
- TanStack Query 完整指南 — 服务端状态深度实践
- Zustand 官网文档
- Jotai 官方文档
- Redux Toolkit 官方文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。