状态管理是前端架构里最容易失控的地方。本章教你一套方法论:先分类 → 再定位 → 后选型,并用 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 的概率。
状态管理考的是「判断力」,每题都说出你的判断依据。