Zustand — 极简 React 状态管理
是什么
Zustand 是一个用一个 hook 就能在任意 React 组件读 / 改全局状态的库。日常类比:
以前 Redux 是去政府部门盖章——你得先写 action(申请单)、再过 reducer(窗口审核)、最后 dispatch(盖章)、组件再 selector(领回执)。Provider 是大门,少了进不去。
Zustand 是直接打个电话——
const count = useStore(s => s.count),一个 hook 全搞定。没有大门、没有窗口、没有申请单。
它的极简 API 就是两件事:
import { create } from 'zustand'
// 1. create 创建 store——state 和 actions 写一起const useStore = create((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })),}))
// 2. 任意组件用 hook 订阅function Counter() { const count = useStore((s) => s.count) return <button onClick={() => useStore.getState().inc()}>{count}</button>}没 Provider、没 reducer、没 actionType——这就是 Zustand 全部表面积。
为什么重要
不理解 Zustand,下面这些事都没法解释:
- 为什么 Stack Overflow 2024 调查里 Zustand 用户量反超 Redux——5 年时间从无名到第一
- 为什么 React 生态 2024 后默认推荐”服务端态用 tanstack-query / 客户端态用 Zustand”的双轨制
- 为什么 1KB 的库能干掉 10KB 的 Redux Toolkit——少 = 强是这里的真理
- 为什么 pmndrs(Three.js 生态那群人)的库都”反 Provider”——他们做 react-three-fiber 时被 Context 跨 renderer 失效坑过
一句话:Zustand 证明了大型 React 库不一定靠”加更多概念”取胜,靠”减更多依赖”也能赢。
核心要点
Zustand 的心智模型只有 三件事:
-
create 创建 store:把 state(数据)和 actions(修改 state 的函数)写在同一个对象里。类比:把数据和操作打包成一个”小型部门”。
-
useStore(selector) 订阅:组件用 selector 告诉 Zustand”我只关心哪一片”。比如
s => s.count——只有 count 字段变了才触发当前组件 re-render,其他字段变化无感。 -
middleware 链:用高阶函数串联
persist(自动存 localStorage)/immer(让你”直接 mutate”)/devtools(接 Redux DevTools)。每个 middleware 是(initializer) => initializer的纯函数包装。
三件事加起来 ≈ 1KB(gzip 后)。
实践案例
案例 1:最简 store——counter
import { create } from 'zustand'
const useStore = create((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })),}))
function Counter() { const count = useStore((s) => s.count) const inc = useStore((s) => s.inc) return <button onClick={inc}>{count}</button>}逐部分解释:
create((set) => ({...})):用户传入的 initializer 函数,Zustand 把 setState 注入给你set((s) => ({ count: s.count + 1 })):拿到当前 state 函数式更新(推荐写法)useStore((s) => s.count):selector 提取 count,只有 count 变才 rerender- 没 Provider、没 import store——任何文件 import
useStore即用
案例 2:persist 中间件——白送本地持久化
import { create } from 'zustand'import { persist } from 'zustand/middleware'
const useUserStore = create( persist( (set) => ({ userId: null, setUser: (id) => set({ userId: id }), }), { name: 'app-user' } // localStorage 的 key ))刷新页面,userId 自己回来了。零配置。
案例 3:分离 selector 引用,避免不必要 rerender
// 错误写法:每次 render 都新对象引用,永远 rerenderconst user = useStore((s) => ({ name: s.name, age: s.age }))
// 正确写法 1:用 useShallow 浅比较import { useShallow } from 'zustand/react/shallow'const user = useStore(useShallow((s) => ({ name: s.name, age: s.age })))
// 正确写法 2:分两次取const name = useStore((s) => s.name)const age = useStore((s) => s.age)selector 返回新对象引用 = 每次都不同 = 每次都 rerender——这是 Zustand 最常见的性能坑。
踩过的坑
-
不传 selector 直接用
useStore():会订阅整个 store,任何字段变都触发当前组件 rerender。永远传 selector,哪怕只是s => s。 -
selector 返回新对象引用:
s => ({ a: s.a, b: s.b })每次 render 都是新对象 → React 看到引用变 → 永远 rerender。要么用useShallow、要么拆成多次单字段取。 -
store 之间共享状态:Zustand 没内建”组合多 store”的方案。两个 store 想共享一片状态,要么手动 subscribe 互相写、要么合并成一个 store。这是 jotai 的原子化模型反而更顺手的场景。
-
React Server Components 不能直接用:Zustand 依赖
useSyncExternalStore(Client-only hook)。RSC 里用 Zustand 必须在'use client'组件内,不能在 server component 里useStore()。 -
selector 写在组件内引用不稳定:
useStore(s => s.count)里s => s.count每次 render 都是新函数。Zustand 内部用useCallback([api, selector])包装——selector 引用变 = 重新订阅一次。小项目无感,10k 组件订阅时会塌陷。把 selector 提到组件外(const selectCount = s => s.count)能避免。
适用 vs 不适用场景
适用:
- 中小型 React 项目的全局客户端态(登录用户、主题、UI 偏好)
- 从 Redux 迁出的第一站——3 个概念替代 5 层 boilerplate
- 需要不依赖 Provider 的库内部 store(react-three-fiber 这类)
- 与 tanstack-query 配合:服务端态用 query / 客户端态用 Zustand
不适用:
- 服务端返回的接口数据 → 用 tanstack-query / SWR(缓存、重新请求、乐观更新都白送)
- 高频原子化更新(每次只动一个字段,几百个字段并存)→ 用 jotai 的 atom 模型
- 跨进程 / 跨 tab 同步 → Zustand persist 只覆盖 localStorage,多 tab 同步要自己接 BroadcastChannel
- 复杂多步表单 / 状态机 → 用 xstate,状态转换显式声明
历史小故事(可跳过)
- 2019 年:Paul Henschel(pmndrs 创始人)写 react-three-fiber 时被 Context 跨 renderer 失效坑过,开始想”状态管理能不能不依赖 React”。
- 2019 年底:Zustand v1 发布,核心 vanilla store 101 行 TypeScript,零依赖。
- 2022 年:React 18 引入
useSyncExternalStore,Zustand v4 接入,正式解决 React 并发模式的 tearing 问题。 - 2024 年:Stack Overflow 调查显示 Zustand 用户量首次反超 Redux。
- 2026 年:v5 当家,仍是 ~1KB,仍是 0 生产依赖。
5 年时间从”小众玩具”到”事实标准”。
学到什么
- 状态管理可以不绑死 React——把 store 做成普通 JS 对象,React 只是订阅者之一。这是 Zustand 比 Redux 更”基础”的原因。
- selector + 引用相等是 React 性能优化的本质——任何”全局态库”都得回答”组件什么时候 rerender”,Zustand 选了最直白的”selector 返回值变 = rerender”。
- API 表面积少 = 心智负担少——Redux 的 5 层概念在 Zustand 是 3 个,差距来自”砍掉历史包袱”而非”创新”。
- 库的合理拆层:vanilla(纯 JS)→ React 适配(64 行)→ middleware 高阶器。每层都能独立讲清楚。
延伸阅读
- 官方 docs:zustand.docs.pmnd.rs(短小精悍,1 小时读完)
- 视频教程:Jack Herrington — Zustand vs Redux Toolkit(30 分钟看完两边代码量对比)
- 自己写实现:照着 vanilla.ts 100 行手写一遍,再对照 react.ts 64 行接 useSyncExternalStore——能讲清楚 Zustand 内核就懂了大半状态管理库
- tanstack-query —— 服务端态的标配,与 Zustand 分工
- react-hooks ——
useSyncExternalStore是 Zustand React 接入的桥梁
关联
- tanstack-query —— 服务端态用 query / 客户端态用 Zustand 的双轨制
- react-hooks ——
useSyncExternalStore是 React 18 给外部 store 的官方接入点 - jotai —— 同 pmndrs 出品但用原子化模型,适合高频细粒度更新
- xstate —— 复杂状态转换的状态机方案,与 Zustand 形成互补
反向链接
- immer —— Immer — 用 Proxy 让你写”看起来可改”的代码却产出不可变状态
- jotai —— Jotai — 原子化 React 状态管理
- nanostores —— nanostores — 不到 1 KB 的”框架无关”状态库
- react —— React UI 组件库
- react-native —— React Native — 用 React 写、编译成真正的原生 App
- solid —— SolidJS — 细粒度响应式 UI 框架
- swr —— SWR — React 远程数据 hook 的极简流派
- tanstack-form —— TanStack Form — 跨框架共享一份表单校验逻辑
- tanstack-query —— TanStack Query — 数据获取与缓存库
- valtio —— valtio — 让 state.x++ 直接驱动 React 重渲染的 Proxy 状态库
- xstate —— XState — 把状态画成图,让矛盾写不出来