状态管理是前端架构里最容易失控的地方。本章教你一套方法论:先分类 → 再定位 → 后选型,并用 Zustand、Redux Toolkit、服务端状态库讲透「到底怎么落地」。学完本章,任何状态你都能说出它该放哪、为什么。
90% 的状态管理混乱,都是因为「没有分类直接开写」。
| 类型 | 例子 | 归宿 |
|---|---|---|
| 服务端状态 | 列表数据、详情、用户信息 | 请求缓存库(数据请求库) |
| 客户端全局状态 | 主题、语言、登录态、权限 | 轻量全局库或 Context |
| 路由状态 | URL 参数、查询条件、tab | 路由库(尽量放 URL) |
| 表单状态 | 输入值、校验、提交状态 | 表单库或组件内部 |
| 局部 UI 状态 | 弹窗开关、折叠、hover | 组件内部 state |
补充判断:刷新要保留 → URL 或持久化;数据来自服务端 → 请求缓存库,不进全局 store;只有组件自己用 → 永远别全局化。
当前中小项目的主流选择,样板代码最少。
// 创建 store(一个函数返回状态与动作)
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [], // 状态
addItem: (item) => // 动作:必须用 set 更新
set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));
// 组件里使用:选择器订阅,只取自己需要的
function CartCount() {
const count = useCartStore((s) => s.items.length); // 只有 count 变了才重渲染
return <span>共 {count} 件</span>;
}
s => s.items.length 这个计算结果,store 变化时用浅比较判断——没变就不通知。比 Context 的「一改全通知」精准得多。
// ⚠️ 陷阱:选择器返回新对象会无限重渲染!
// ❌ 每次调用都返回新对象,浅比较永远不相等 → 死循环
const data = useCartStore((s) => ({ total: s.items.length, list: s.items }));
// ✅ 用 useShallow 做浅比较
import { useShallow } from "zustand/react/shallow";
const data = useCartStore(useShallow((s) => ({ total: s.items.length, list: s.items })));
// 持久化:一行中间件搞定
import { persist } from "zustand/middleware";
const useStore = create(persist(
(set) => ({ theme: "light", toggle: () => set(s => ({ theme: s.theme === "light" ? "dark" : "light" })) }),
{ name: "theme-storage" } // localStorage 的 key
));
Redux 的定位是「严格约束 + 可预测」,适合复杂全局状态与大团队。
单向数据流:action → reducer → store → UI。数据流向清晰,配合 DevTools 可以时间旅行调试。
// slice = 一组状态 + 动作 + reducer 的合集
import { createSlice, configureStore } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
addItem(state, action) { // 内部用 immer,可以「看似直接改」
state.items.push(action.payload);
},
removeItem(state, action) {
state.items = state.items.filter(i => i.id !== action.payload);
},
},
});
// 自动生成 actions:cartSlice.actions.addItem
// 自动生成 reducer:cartSlice.reducer
const store = configureStore({ reducer: { cart: cartSlice.reducer } });
// 组件里使用
const items = useSelector(s => s.cart.items); // 订阅
const dispatch = useDispatch();
dispatch(addItem({ id: 1, name: "苹果" })); // 派发
现代前端最推荐的「数据层」方案,把请求当状态管理。
请求缓存库(数据请求库 / 请求缓存库,如数据请求库 Query、SWR)的核心思想:把「获取数据」当成「读缓存」——数据来了进缓存,多个组件共享同一份缓存,不会重复请求;数据失效了自动重新请求。
// 数据请求库 Query 基本用法
import { useQuery } from "@tanstack/react-query";
function UserList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ["users"], // 缓存键:同键共享
queryFn: fetchUsers, // 请求函数
staleTime: 60_000, // 60 秒内数据「新鲜」,不重新请求
});
if (isLoading) return <Loading />;
if (error) return <ErrorView />;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// 修改后使缓存失效,自动重新拉取(关键能力)
const queryClient = useQueryClient();
await saveUser(user);
queryClient.invalidateQueries({ queryKey: ["users"] });
两类容易被忽视的状态,管好它们体验立涨。
// React Hook Form:性能好(非受控 + ref)、样板少
import { useForm } from "react-hook-form";
function RegisterForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("username", { required: "必填", minLength: { value: 3, message: "至少 3 个字符" } })} />
{errors.username && <p>{errors.username.message}</p>}
<button type="submit">注册</button>
</form>
);
}
useSearchParams 读写 URL 参数。// 用 URL 管理筛选状态
const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get("page") || "1");
const keyword = searchParams.get("q") || "";
setSearchParams({ page: "2", q: "react" }); // 更新 URL,组件自动重渲染
本章项目验证「方法论」是否真的用起来了。
要求:① 商品列表来自服务端状态(数据请求库,含加载/错误/重试);② 购物车是全局状态(Zustand);③ 结算表单用表单库;④ 商品分类筛选放 URL;⑤ 登录态放全局状态 + 持久化。做完后,画一张「每个状态放哪、为什么」的表——这才是本章真正的交付物。
找出自己项目里「乱放」的状态(比如组件内管服务端数据、useState 传了十几层),按方法论重构。记录重构前后的对比:代码量、重渲染次数、出 bug 的概率。
状态管理考的是「判断力」,每题都说出你的判断依据。
答案:
答案:服务端数据有自己完整的生命周期(加载、缓存、失效、重试、并发去重、请求取消),这些能力请求缓存库内置提供。塞进全局 store 意味着你要手写所有上述逻辑——既重复造轮子又容易出错。全局 store 的正确定位是管「客户端的、自己产生的」状态(主题、登录态等),不是服务端的「远程数据的本地镜像」。
答案:按以下顺序逐层判断:
答案:选择器通过「精准订阅 + 浅比较」提升性能:组件只订阅自己需要的那个值(如 state => state.items.length),store 更新时对选择器返回值做浅比较——没变就不触发重渲染。
陷阱:选择器如果每次都返回 新引用(如 { total, list }),浅比较永远不相等 → 导致组件无限重渲染(死循环)。
useShallow 做浅比较替代默认的 === 比较。useShallow((s) => ({ total: s.items.length, list: s.items })) 会逐字段比较,只有字段值变化才触发重渲染。
答案:
三大原则:
单向数据流环节:
答案:解决了旧版 Redux 的四大痛点:
{...state, nested: {...state.deep}}。内部集成 Immimer,允许「看似直接修改」的写法,RTK 自动转为不可变更新。configureStore 自动配置 DevTools、常用 middleware。答案:
queryKey 的作用:作为缓存的唯一标识。相同 queryKey 的请求共享缓存(组件复用同一数据、避免重复请求);不同 queryKey 视为不同缓存(如 ["users", 1] 和 ["users", 2] 是两条记录)。同时用于 invalidateQueries 精准失效缓存。
staleTime vs gcTime:
| 配置 | 含义 | 默认值 |
|---|---|---|
| staleTime | 数据「新鲜」的时间窗口。窗口内再次挂载不重新请求,超过后才后台重新拉取 | 0(每次 mount 都重新请求) |
| gcTime | 缓存在内存中保留的时间。超过且无订阅者时,缓存被垃圾回收 | 5 分钟 |
答案:乐观更新(Optimistic Update)是一种交互策略:在请求发出去之前,先把 UI 更新为「假设成功」的形态;请求成功则保持结果,失败则回滚到更新前的状态并提示用户。
适用场景:高延迟操作(网络慢)、操作成功率高的场景(点赞、标记已读、加入购物车)。用户感知「零延迟」,体验远超等接口返回才更新。
onMutate 中取消当前 queries、写入乐观数据;onError 中用 context 回滚;onSettled 中 invalidateQueries 重新拉取确保最终一致。
答案:三个核心价值:
答案:
| 状态 | 类型 | 归宿 | 理由 |
|---|---|---|---|
| 购物车(跨页面) | 客户端全局状态 | Zustand + persist 持久化 | 跨页面共享、客户端自产自销、刷新要保留 |
| 商品列表 | 服务端状态 | 请求缓存库(useQuery) | 来自服务端、需要缓存/失效/重试/并发去重 |
| 结算表单 | 表单状态 | React Hook Form(组件内) | 只当前表单用、提交后即销毁、需要校验和受控管理 |