React 状态管理
概述
React 的内置状态机制——useState 和 useReducer——足以应付中小型应用的局部状态。但当应用复杂度上升,状态可能在多个不相关组件间共享、需要持久化、需要支持撤销/重做、或者需要管理异步数据获取时,我们就需要更系统化的状态管理方案。
状态管理的核心问题是:状态应该放在哪里?谁应该拥有它?如何让需要它的组件方便地访问?
这个问题没有 universally correct answer。不同规模、不同团队、不同业务场景下,最优方案差异很大。理解各方案的设计哲学和 trade-off,比记住"某个场景用 X"更重要。
状态分类框架
在讨论具体方案之前,我们需要先理解状态的分类。因为不同类型的状态,适合用不同的管理方案。
Dan Abramov 提出的 You Might Not Need a Tracking Library 一文中,对状态分类有精辟论述:
Think about what makes state "local" — does the component use it and nothing else? Then keep it in useState or useReducer. Only move it somewhere else when you genuinely need to share it.
四类状态
| 状态类型 | 特征 | 存储位置 |
|---|---|---|
| UI 状态 | 仅影响单个组件渲染 | useState / useReducer |
| 跨组件状态 | 多个相关组件共享 | Context / 状态管理库 |
| 服务端状态 | 从 API 获取,会过期,需要缓存 | TanStack Query / SWR |
| 持久化状态 | 跨会话保存(localStorage、URL) | 专门的持久化方案 |
关键洞察:服务端状态是最容易被忽视的一类。很多时候所谓的"状态管理问题",本质上是"数据获取和缓存问题"。
Redux 核心概念
设计哲学
Redux 遵循三大原则:
- Single Source of Truth:整个应用的 state 存储在一个只读的 store 中
- State is Read-Only:不能直接修改 state,必须通过 dispatch action 来描述"发生了什么"
- Reducers are Pure:用纯函数计算新状态,保证可预测性
这种架构叫做 Unidirectional Data Flow(单向数据流)。
核心概念:Store
Store 是 Redux 的核心对象,它:
- 持有应用的完整 state 树
- 提供
getState()读取状态 - 提供
dispatch(action)更新状态 - 提供
subscribe(listener)注册监听器
import { createStore } from 'redux';
const store = createStore(rootReducer);
store.subscribe(() => {
console.log('State changed:', store.getState());
});
store.dispatch({ type: 'INCREMENT' });
// State changed: { counter: 1 }
核心概念:Reducer
Reducer 是一个纯函数,接收当前状态和 action,返回新状态:
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
};
Redux 的核心约束:Reducers 必须是纯函数。
- 相同输入 → 相同输出
- 不应有副作用(API 调用、Mutation)
- 不应调用非纯函数(如
Date.now())
核心概念:Action
Action 是一个普通对象,描述"发生了什么":
// Action Creator
const increment = () => ({ type: 'INCREMENT' });
const addTodo = (text) => ({
type: 'ADD_TODO',
payload: { id: Date.now(), text, completed: false }
});
// Dispatch
store.dispatch(increment());
store.dispatch(addTodo('Learn Redux'));
Action 是数据的唯一入口。好处是所有状态变更都被记录下来,支持时间旅行调试、撤销/重做等高级功能。
Middleware:扩展 Redux
Middleware 是 Redux 的扩展机制,位于 dispatch 和 reducer 之间,可以:
- 处理异步逻辑
- 记录日志
- 路由集成
- 任何 side effect
// 自定义 middleware 签名
const logger = (storeAPI) => (next) => (action) => {
console.log('Before:', storeAPI.getState());
let result = next(action); // 继续传递 action
console.log('After:', storeAPI.getState());
return result;
};
// 应用 middleware
import { applyMiddleware } from 'redux';
const store = createStore(rootReducer, applyMiddleware(logger, thunk));
最常用的 middleware:
redux-thunk:支持异步 action creatorredux-saga:使用 Generator 处理复杂异步流程redux-logger:开发环境日志redux-toolkit:Redux 官方推荐的现代 Redux 编写方式
Redux Toolkit:现代 Redux 写法
Redux Toolkit(RTK)是官方推荐的 Redux 编写方式,解决了"样板代码过多"的问题:
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
// RTK 允许直接修改 state(内部使用 Immer)
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
// 自动生成 action creators
store.dispatch(increment());
console.log(store.getState().counter.value); // 1
RTK 的核心改进:
- createSlice:自动生成 action creators 和 action types
- Immer:允许"可变"语法书写不可变更新
- configureStore:简化 store 配置,自动添加 middleware
- RTK Query:内置的数据获取和缓存方案
Redux 的适用场景
Redux 并不是万能的。它的优势在于:
- 复杂的中大型应用
- 需要时间旅行调试
- 需要撤销/重做
- 状态变更需要可追溯
- 团队需要统一的状态管理模式
Redux 的不足:
- 样板代码仍然较多(RTK 改善了这一点)
- 需要额外学习成本(actions、reducers、middleware)
- 对于简单场景过于重量
Zustand:轻量级方案
设计理念
Zustand 是一个轻量级的状态管理库,核心设计理念是:状态管理不应该复杂。
与 Redux 相比,Zustand 没有 Provider、没有 action/reducer 分离、没有 middleware 概念。状态、更新逻辑、订阅机制都在一个 store 定义中。
import { create } from 'zustand';
const useStore = create((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
reset: () => set({ bears: 0 }),
}));
// 在组件中使用
function BearCounter() {
const bears = useStore((state) => state.bears);
const increase = useStore((state) => state.increase);
return (
<div>
<p>{bears} bears</p>
<button onClick={increase}>Increase</button>
</div>
);
}
Zustand 的核心特性
1. 去中心化 Store
Redux 推荐单一 store,而 Zustand 允许创建多个独立的 store:
const useAuthStore = create((set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}));
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));
2. 精确订阅
Zustand 的 selector 机制确保只有变化的状态才会触发重新渲染:
// 只有 bears 变化才重新渲染
const bears = useStore((state) => state.bears);
// 使用 shallow 比较数组
const { bears, fishes } = useStore(
(state) => ({ bears: state.bears, fishes: state.fishes }),
shallow
);
3. 开发者体验
- TypeScript 支持良好
- 内置
subscribeWithSelectormiddleware - 支持持久化(通过 plugin)
- 支持 React 外部使用(vanilla store)
Zustand vs Redux
| 维度 | Zustand | Redux |
|---|---|---|
| 包大小 | ~1KB | ~7KB(不含 middleware) |
| 概念复杂度 | 低 | 中 |
| Boilerplate | 极少 | 较多(RTK 改善) |
| DevTools | 支持 | 完整支持 |
| 中间件 | 插件化 | middleware |
| 适用规模 | 中小型 | 中大型 |
Jotai:原子化状态管理
设计理念
Jotai 的核心概念是原子(Atom)。每个 atom 是一个独立的最小状态单元,组件直接订阅需要的 atom,而不是订阅整个 store。
import { atom, useAtom } from 'jotai';
// 基础 atom
const countAtom = atom(0);
// 派生 atom(类似 computed)
const doubledAtom = atom((get) => get(countAtom) * 2);
// 在组件中使用
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
原子化的优势
1. 精确重渲染
Jotai 使用 atom 作为重渲染的最小单元。只有订阅的 atom 变化才会触发更新:
// 组件 A 只订阅 countAtom
function ComponentA() {
const [count] = useAtom(countAtom); // 只依赖 countAtom
}
// 组件 B 只订阅 nameAtom
function ComponentB() {
const [name] = useAtom(nameAtom); // 只依赖 nameAtom
}
// countAtom 变化时,只有 ComponentA 重渲染
2. 派生状态自动计算
派生 atom 只有在被至少一个组件订阅时才会重新计算:
const expensiveAtom = atom((get) => {
// 只有被使用时才会执行
return computeExpensiveValue(get(basicAtom));
});
3. 异步支持
const userDataAtom = atom(async (get) => {
const userId = get(userIdAtom);
return fetchUser(userId);
});
function UserProfile() {
// Jotai 自动处理 Promise
const [user] = useAtom(userDataAtom);
if (user === undefined) return <Skeleton />;
if (user === null) return <Error />;
return <div>{user.name}</div>;
}
Jotai vs Zustand vs Redux
| 维度 | Jotai | Zustand | Redux |
|---|---|---|---|
| 状态单元 | Atom(最小单元) | Store(可多个) | Single Store |
| 重渲染粒度 | Atom 级别 | Store 级别 | Store 级别 |
| 学习曲线 | 低-中 | 低 | 中 |
| 派生状态 | 内置 atom | 手动 selector | selector/mapState |
| 异步 | 原生支持 | 需 middleware | 需 middleware |
| 适用场景 | 细粒度状态共享 | 中小型应用 | 中大型应用 |
TanStack Query:服务端状态管理
问题背景
服务端状态(从 API 获取的数据)与客户端状态有本质不同:
| 特性 | 客户端状态 | 服务端状态 |
|---|---|---|
| 更新频率 | 由用户交互决定 | 由服务器数据决定 |
| 持久性 | 通常是临时的 | 应该是持久的 |
| 同步性 | 总是本地最新 | 可能与服务器不同步 |
| 获取方式 | 直接读写 | 需要异步获取 |
| 缓存 | 不需要 | 需要 |
核心问题:传统的状态管理库(Redux、Zustand、Jotai)都不是为服务端状态设计的。用它们管理 API 数据,要么自己实现缓存/失效逻辑,要么产生大量样板代码。
TanStack Query(原 React Query)解决方案
TanStack Query 是专门为服务端状态设计的库,核心功能:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }) {
// 自动处理:加载状态、错误、缓存、重新获取
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 分钟内不重新获取
});
if (isLoading) return <Skeleton />;
if (error) return <Error error={error} />;
return <div>{user.name}</div>;
}
// 数据修改后自动使缓存失效
function UpdateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => updateUser(newUser),
onSuccess: () => {
// 清除相关缓存,触发重新获取
queryClient.invalidateQueries({ queryKey: ['user'] });
},
});
return (
<button onClick={() => mutation.mutate({ name: 'New Name' })}>
Update
</button>
);
}
TanStack Query 的核心概念
1. Query:数据获取
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
enabled: !!userId, // 条件执行
select: (data) => data.filter(t => t.completed), // 数据转换
});
2. Mutation:数据修改
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: (newTodo) => {
// Optimistic update 或缓存更新
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
},
onError: (error, variables, context) => {
// 错误处理,回滚操作
},
});
3. 缓存策略
// 缓存自动清理配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 数据新鲜时间
gcTime: 10 * 60 * 1000, // 缓存回收时间
refetchOnWindowFocus: true, // 窗口聚焦时重新获取
retry: 3, // 失败重试次数
},
},
});
TanStack Query vs Redux
| 维度 | TanStack Query | Redux |
|---|---|---|
| 设计目标 | 服务端状态 | 通用状态 |
| 缓存管理 | 内置 | 需手动实现 |
| 重新获取 | 自动 | 需手动触发 |
| 加载状态 | 内置 | 需手动管理 |
| 错误处理 | 内置 | 需手动管理 |
| API 响应 | 原始数据 | 处理后的数据 |
选择决策框架
按状态类型选择
状态类型
├── 组件本地状态
│ └── useState / useReducer
├── 跨组件共享的简单状态
│ └── Context
├── 需要高性能细粒度更新的状态
│ └── Jotai
├── 中小型应用的状态共享
│ └── Zustand
├── 复杂中大型应用的状态管理
│ └── Redux Toolkit
├── 服务端数据(API 响应)
│ └── TanStack Query
└── 需要持久化的状态
└── Zustand + persist / Redux + redux-persist
按团队规模选择
小型团队 / 简单项目:
- 优先用
useState+useReducer - 需要共享时用 Context
- 服务端数据直接 fetch,状态简单
- 过度设计是最大的敌人
中型团队 / 中等复杂度项目:
- Zustand 作为主力状态管理
- TanStack Query 处理服务端状态
- 避免 Redux 的过度复杂性
- 平衡简洁性和可扩展性
大型团队 / 复杂项目:
- Redux Toolkit 提供规范性和可追溯性
- TanStack Query 处理所有数据获取
- 明确的 action 命名和 reducer 模式
- 时间旅行调试和状态追溯的价值显现
混合使用是常态
在实际项目中,很少只用一个方案:
// 组合使用示例
function App() {
return (
<QueryClientProvider client={queryClient}>
<PersistGate persistor={persistor}>
<Provider store={store}>
<Router />
</Provider>
</PersistGate>
</QueryClientProvider>
);
}
- TanStack Query:服务端状态
- Redux Toolkit:全局客户端状态(用户信息、UI 状态)
- Zustand:轻量级独立模块(购物车、播放器)
- useState:组件本地临时状态
常见问题与解决方案
问题 1:Context 导致的额外渲染
Context 的问题是:任何 Context 值变化,所有消费组件都会重新渲染。
解决方案:
// 1. 拆分 Context
const UserContext = createContext(null);
const ThemeContext = createContext(null);
// 2. 使用 useContextSelector(jotai/react-query 方案)
const user = useUser(); // 只订阅 user 变化
问题 2:状态同步 vs 状态管理
很多"状态管理问题"本质上是状态同步问题:
// 错误:在两个地方存储相同的数据
const [user, setUser] = useState(null);
localStorage.setItem('user', JSON.stringify(user));
// 正确:单一数据源 + 持久化层处理同步
// 状态管理库负责状态,持久化库负责同步
问题 3:API 状态与 UI 状态混合
// 错误:把 API 响应存在 Redux 里
const initialState = {
users: [], // API 数据
loading: false, // UI 状态
error: null, // UI 状态
selectedId: null // UI 状态
};
// 正确:分离关注点
// TanStack Query 管理 users、loading、error
// Redux(或其他库)只管 selectedId
延展阅读
- Redux 官方文档 — Redux Fundamentals — Redux 核心概念完整讲解
- TanStack Query 官方文档 — 服务端状态管理权威指南
- Zustand 官方文档 — 轻量级状态管理的最佳实践
- Jotai 官方文档 — 原子化状态管理的理念与实现
- Kent C. Dodds — State Management at Scale — 不同规模下状态管理方案的选择