状态管理实战:从 useState 到 Zustand
理解 props drilling 痛点,用 Zustand 建全局 store 与 selector 订阅
- 理解 props drilling 是什么、为什么讨厌
- 知道 Context 的局限
- 会用 Zustand 建全局 store(进度/主题/登录态)
- 理解 selector 订阅为什么能避免无关重渲染
可以把全局 store 想象成一个大家都能读写的共享字典,再配一个「通知员」:字典里某一格变了,只通知订阅了那一格的组件。这和 Python 里一个全局状态对象 + 一堆监听回调是一回事,谁关心哪块数据,谁就登记一下。
props drilling 的痛
React 默认数据靠 props 一层一层往下传。如果一个深层孙子组件要用登录态,就得从顶层组件一路把 login 状态透传三四层——中间那些组件根本用不到,却要帮忙转发。这就叫 props drilling(属性逐层钻取)。
// 痛点示意:login 状态要穿过根本不需要它的 B、C 组件
<App login={user}> // 顶层有登录态
<Nav login={user}> // 只是帮忙转发
<Avatar login={user}> // 真正用到的是它
</Nav>
</App>Context 的局限与 Zustand 的出现
Context 能解决 drilling,但它有个毛病:一旦 Context 值变了,所有用了它的组件都会重渲染,哪怕你只需要其中一个字段。Zustand 是一个极简的全局状态库,核心思路是:建一个 store,组件用 selector 只订阅自己需要的那一小片数据,那片没变就不重渲染。
// Zustand 写法(概念示意,真实项目 import { create } from "zustand")
import { create } from "zustand";
const useStore = create((set) => ({
progress: 0,
theme: "light",
setProgress: (n) => set({ progress: n }),
toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
}));
// 组件里:只订阅 progress,theme 变了不会让它重渲染
function ProgressBar() {
const progress = useStore((s) => s.progress);
return <div>{progress}%</div>;
}这里最关键的细节是 selector 返回的那一小片值:Zustand 默认用 Object.is 比较「这次 selector 的结果」和「上次 selector 的结果」,只有这一小片真的变了才触发重渲染。如果你图省事写 useStore((s) => s)——把整个 state 都订阅回来,那无论哪个字段变,这个组件都会重渲染,selector 就白写了。记住一个原则:selector 返回的越精确,多余渲染越少。
什么不该放进全局 store
全局状态不是垃圾桶。一旦什么都往里塞,store 会变成一个谁都能改、改了到处影响的黑盒,调试时根本说不清是谁改的、界面为什么变。判断标准很简单:这个状态是不是「多个互不嵌套的组件、跨好几层都要读写」。登录态、主题色、全局学习进度符合;而一个弹窗的开关、一个输入框的内容、鼠标 hover 在哪个卡片上,只在局部组件间流转,放全局就是过度设计。
常见翻车:把表单字段全塞进全局 store,结果改一个字整个页面所有订阅组件都重渲染;或者把服务端缓存(接口返回的列表)也塞进全局 store,结果和 react-query 这类缓存两套真相打架。经验法则:能用 useState 解决的局部状态就别上全局;服务端数据交给 react-query/swr,全局 store 只管「真正的前端共享状态」。
Context / Redux / Zustand 怎么选
三者都是解决共享状态,差别在复杂度和精确订阅能力。Context 适合「低频变化」的值(主题、登录用户),因为它一变所有消费者都重渲染,不适合高频更新。Redux 功能最全、生态成熟、有 DevTools 时间旅行,但样板代码多、概念重,小项目用着累。Zustand 极简、API 就一个 create、内置 selector 精确订阅,是现在中小型 React 项目的甜点选择。
低频全局值(主题/用户)用 Context 就够;高频、跨层、需要精细控制重渲染的用 Zustand;大型团队、需要严格单向数据流和完整工具链的才上 Redux。本项目选了 Zustand,正是看中它「够用且不啰嗦」。
Zustand 里发请求也很自然:store 的 action 里直接 await 接口,拿到数据后 set 进 state 就行,不需要 Redux 那种 action/reducer/thunk 的层层包装。想让状态刷新页面后还在,加一个持久化中间件把指定字段写进 localStorage 即可。这些是进阶用法,现在知道 store 既能存值、也能存改值的方法、还能承载异步流程就够了。
动手:用 Python 模拟 store + 订阅者
下面模拟「store + selector + 只在订阅片变化时通知」。注意:改 progress 时只有进度条收到通知,顶栏不动;改 theme 时才轮到顶栏。
这个真实项目(朋友好学)的全局进度、主题、登录态就是用 Zustand 管的。s8-m4-l3 会带你逐行解析项目里的 store 写法;s13-m2-l3 再讲 Zustand 和 Redux、Context 到底怎么选型。现在先记住:共享、跨层、频繁用的状态才进全局 store。
什么时候才该用全局状态库(如 Zustand)?
资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
穿透是反复查「根本不存在」的数据,用布隆过滤器或缓存空值挡;击穿是某个热点 key 过期瞬间大量请求压向数据库,用互斥重建或逻辑过期;雪崩是大批 key 同时过期,给 TTL 加随机抖动。缓存与库的一致性常用「先更新数据库、再删除缓存」,并靠重试/订阅补偿。
挑战任务
实现一个最简订阅 store
全局 store 的核心是「set 时通知所有订阅者」。实现 make_store(),返回 state、subscribe(fn)、set_n(v) 三个部分:set_n 改值后依次调用所有登记的 fn。
课后作业
给状态分类:进全局还是留局部
全局 store 只放跨多层共享的状态,别什么都塞。实现 is_global(state):state 在 GLOBAL 清单(auth_user/theme/global_progress)里返回 True,在 LOCAL 清单(modal_open/input_draft/hover_card)里返回 False。逐个打印状态与判定。