首页  |  学习总览  |  ← 上一章:框架核心 阶段六 · 第 6 章,共 10 章
阶段六 · 预计 60 小时

状态管理与数据流

状态管理是前端架构里最容易失控的地方。本章教你一套方法论:先分类 → 再定位 → 后选型,并用 Zustand、Redux Toolkit、服务端状态库讲透「到底怎么落地」。学完本章,任何状态你都能说出它该放哪、为什么。

1状态管理方法论(先学思路再学工具)

90% 的状态管理混乱,都是因为「没有分类直接开写」。

1.1 状态的五种类型

类型例子归宿
服务端状态列表数据、详情、用户信息请求缓存库(数据请求库)
客户端全局状态主题、语言、登录态、权限轻量全局库或 Context
路由状态URL 参数、查询条件、tab路由库(尽量放 URL)
表单状态输入值、校验、提交状态表单库或组件内部
局部 UI 状态弹窗开关、折叠、hover组件内部 state
💡 最常犯的错 把服务端数据塞进全局 store(Redux/Zustand)是教科书级反模式。服务端数据有自己的生命周期(加载、缓存、失效、重试、并发去重),这些能力请求缓存库内置,手写既重复又易错。

1.2 状态定位决策树(背下来)

状态属于谁?
只有一个组件用?
→ 组件内部 state
兄弟组件共享?
→ 提升到共同父级
深层跨级?
→ Context
无关模块共享?
→ 全局状态库

补充判断:刷新要保留 → URL 或持久化;数据来自服务端 → 请求缓存库,不进全局 store;只有组件自己用 → 永远别全局化。

2Zustand:轻量全局状态库

当前中小项目的主流选择,样板代码最少。

2.1 基本用法

// 创建 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>;
}
💡 Zustand 为什么性能好 核心是「选择器 + 浅比较」:组件订阅的是 s => s.items.length 这个计算结果,store 变化时用浅比较判断——没变就不通知。比 Context 的「一改全通知」精准得多。

2.2 选择器陷阱与持久化

// ⚠️ 陷阱:选择器返回新对象会无限重渲染!
// ❌ 每次调用都返回新对象,浅比较永远不相等 → 死循环
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
));
3Redux Toolkit:大团队的严谨之选

Redux 的定位是「严格约束 + 可预测」,适合复杂全局状态与大团队。

3.1 三大原则与核心概念

  • 单一数据源:整个应用只有一棵状态树,存在一个 store 里。
  • 状态只读:不能直接改状态,只能派发 action(描述「发生了什么」)。
  • 纯函数修改:reducer 接收旧状态与 action,返回新状态(必须是纯函数)。
组件
dispatch(action)
Reducer
纯函数计算新状态
Store
保存新状态
组件
订阅并更新

单向数据流:action → reducer → store → UI。数据流向清晰,配合 DevTools 可以时间旅行调试。

3.2 createSlice:现代写法(不用手写 action 类型)

// 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: "苹果" }));      // 派发
💡 RTK Query:官方服务端状态方案 Redux Toolkit 内置的 RTK Query 提供了缓存、失效、轮询、乐观更新等完整能力——如果你团队已用 Redux,服务端状态用它而不要手写 thunk。
4服务端状态:请求缓存库

现代前端最推荐的「数据层」方案,把请求当状态管理。

4.1 核心心智:缓存优先

请求缓存库(数据请求库 / 请求缓存库,如数据请求库 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"] });

4.2 进阶能力(面试加分)

  • 乐观更新:先更新 UI,失败再回滚——体验最好。
  • 请求取消:组件卸载或重复请求时取消旧请求,避免竞态。
  • 重试与退避:网络错误自动重试,指数退避。
  • 窗口聚焦重新请求:回到页面自动刷新数据。
  • 无限滚动:加载更多自动拼接。
⚠️ 使用要点 ① queryKey 是缓存的核心,设计好键的层级(["users", userId]);② 不要把「需要持久化到 URL 的筛选条件」塞进 queryKey(放 URL,路由状态);③ 关注 staleTime 与 gcTime 的区别——前者是「多久算旧」,后者是「缓存多久后被回收」。
5表单状态与路由状态

两类容易被忽视的状态,管好它们体验立涨。

5.1 表单状态:受控表单库

// 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>
  );
}

5.2 路由状态:能放 URL 就别放内存

  • 筛选条件、分页、tab、搜索词 → 放 URL 查询参数(可分享、可刷新、可回退)。
  • React Router 的 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,组件自动重渲染
6实战项目(通关必做)

本章项目验证「方法论」是否真的用起来了。

项目一:购物车应用(状态分层实战)

要求:① 商品列表来自服务端状态(数据请求库,含加载/错误/重试);② 购物车是全局状态(Zustand);③ 结算表单用表单库;④ 商品分类筛选放 URL;⑤ 登录态放全局状态 + 持久化。做完后,画一张「每个状态放哪、为什么」的表——这才是本章真正的交付物。

项目二:给现有项目做状态重构

找出自己项目里「乱放」的状态(比如组件内管服务端数据、useState 传了十几层),按方法论重构。记录重构前后的对比:代码量、重渲染次数、出 bug 的概率。

7通关自检题

状态管理考的是「判断力」,每题都说出你的判断依据。

1. 状态的五种类型是什么?各自归宿在哪?

2. 为什么服务端数据不该放进全局 store?

3. 状态定位决策树的完整流程?

4. Zustand 的选择器为什么能提升性能?返回新对象的陷阱是什么?

5. Redux 的三大原则?单向数据流的五个环节?

6. Redux Toolkit 的 createSlice 解决了旧版 Redux 的什么痛点?

7. 请求缓存库的 queryKey 是干什么的?staleTime 和 gcTime 的区别?

8. 乐观更新是什么?什么时候该用它?

9. 为什么筛选条件要放 URL 而不是内存?

10. 给一个场景:购物车(跨页面)+ 商品列表(服务端)+ 结算表单,三个状态分别怎么管?

✅ 全部通关? 状态架构已心中有数。下一阶段《性能优化实战》,把前面学的渲染、网络、工程化知识全部转化为「可量化的性能提升」。