初识 zustand
什么是 zustand ?
没错,zustand 又是一个基于 React 的状态管理库。Zustand 的发音为 /ˈzuːstænd/。该词源自德语,本义就是"状态"(condition, state),与库的核心功能"状态管理"正好同义,算是一种"直给式"命名。
也许你会问,从初生代的 redux,mobx 和 xstate,再到中生代的其他的状态管理库,如 redux + reduxToolkit,context base, recoil,jotai等,可供选择的 react 的第三方状态管理库已经有了很多,那么我为什么还要选择 zustand 呢?
答案是,zustand 足够简单、快速、高性能,特别适合于中小型项目(大部分人做的就是这种规模的项目),或者需要以特别低的成本快速实现状态管理的场景。
zustand 特点
简单
说它简单,是因为核心的 API 数量只有三个:create()、useBoundStore()、setState()。这使得它非常易于学习和使用,即使是 React 新手也能快速上手。
所谓的「useBoundStore」就是指通过通过闭包来「绑定」了你通过
create()来创建的 zustand store 实例的自定义 hook 函数。这里的"Bound",在理解上要替换为相应的业务领域的 scope 名,比如useUserStore。
而这些核心的 API设计时候的函数签名使用的都是已经被社区普遍接受的心智模型:
create():用于创建一个状态的 store。使用之初,先创建一个 store,这对于使用过 redux 的人群来说,是一个比较熟知的概念。useBoundStore():用于在组件中订阅状态变化并获取状态值。在 react 的 hook 时代,要想使用 store,先 useXxx 一下,这几乎成为了 react 开发者的思维惯性了。而且这与使用 redux 中的useSelector()是非常相似的。setState():用于更新状态值。
说它简单的另一个原因是从 redux 之前的原始范式的视角来讲的。在 redux 的原始范式里面,状态管理的概念和代码是拆分得很细的。这里面有 store, state,reducer,action,dispatch 等。由于 redux 比较推崇单一 store 模式,往往你还要 combine 一下 所有的 reducer。如果再加上它的三层洋葱模型的中间件机制,状态管理的代码就会变得比较复杂了。当然,这么做好处是,你可以非常清晰地看到一个单向的数据流,且状态管理的每一个环节都可以被审视,这对于调试和维护都是非常有帮助的。所以,当前社区的普遍的认知是, redux 的状态管理范式是更适用于大规模的,长期维护的前端项目。
而 zustand 则是基于这个原始范式的一个简化版本。 zustand 只保留了 store 的概念。一切都是围绕 store 展开的。无非就是三部曲:
- 定义 store(包含了 state 和 action) -
StateCreator; - 创建 store -
create; - 在组件中使用 store -
useBoundStore。
灵活
zustand 是很灵活的,因为你可以在任何地方(react 环境和非 react 环境)去调用 setState() 来触发 react 界面更新。它实现灵活的秘诀在于充分利用了 react 原生 API useSyncExternalStore 对非 react 环境触发 state 更新的的支持和 js 中函数也是对象的语言特性。
本文中的「非 react 环境」指的就是「非组件函数作用域」。换句话,就是指那些不能调用 react hook 函数的地方。
一句话,主要你能拿到 zustand store 的 setState() 函数,就可以在任何地方触发 react 界面更新。假设我们有一个下面代码的 store:
js
import create from 'zustand'
const useBoundStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
那么,怎样拿到 setState() handler呢?方法有二。
方法一
zustand 会往 StateCreator 注入 setState() 函数引用。你可以在 StateCreator 中通过直接使用 setState() 函数来定义 action。然后,在组件中使用 useStore() 来获取 action 函数。
js
import { useBoundStore } from './store'
const MyComponent = () => {
const count = useBoundStore((state) => state.count)
const increment = useBoundStore((state) => state.increment)
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
)
}
方法二
zustand 已经利用「函数即对象」的语言特性,把 store 实例的 API (这里面就包含了 setState()方法)挂载到返回的 useStore() 函数上面了。换句话说,你可以直接在组件中使用 useBoundStore.setState() 来访问 setState() 函数。
js
import { useStore } from './store'
const MyComponent = () => {
const count = useStore((state) => state.count)
const setState = useStore.setState
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setState({ count: count + 1 })}>Increment</button>
</div>
)
}
因为 useBoundStore 既是一个 hook, 又是一个普通的函数对象,所以你可以在任何地方调用它,包括非 react 环境。这进一步提高了 zustand 在使用上的灵活度。比如,你完全可以这么用:
js
import { useBoundStore } from './store'
const increment = () =>
useBoundStore.setState((state) => ({ count: state.count + 1 }))
const MyComponent = () => {
const count = useBoundStore((state) => state.count)
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
)
}
高性能
这里的高性能指的是,zustand 实现了真正的「按需渲染」。什么是按需渲染?简单来说,就是只有当组件中使用到的状态值发生变化时,才会触发组件的重新渲染。相比于 redux+ react-redux 的早期版本或者 context provider + useContext 的这种「全量刷新」的渲染方式,zustand 谈得上「高性能」了。
当然,要想实现这个「按需渲染」的功能,还需要作为开发者的你配合 - 使用 selector 来选择你需要的状态。selector 是一个函数,它的参数是 store 的全量 state,返回值是你需要的状态。比如,你只需要 count 状态,就可以这么写:
js
const count = useStore((state) => state.count)
假如你是这么写的话:
js
const MyComponent = () => {
const countStore = useStore()
return (
<div>
<p>Count: {countStore.count}</p>
</div>
)
}
那么就会发生性能回退 - 只要 store 的任意一个 state 发生了变化,哪怕不是 count 的值改变了,组件也会 MyComponent 也会重新渲染。
另外一个注意点是,如果你所订阅的值是一个引用类型,比如对象或者数组,如果还是像上面这种写法的话,那么就很容易发生性能回退(具体的例子可以看官网给出的例子)。
对于这种情况,你就需要再用 useShallow 来包一层。比如:
js
function BearNames() {
const names = useBearFamilyMealsStore(
useShallow((state) => Object.keys(state)),
)
return <div>{names.join(', ')}</div>
}
它相比于 redux 有什么区别?
从源码实现角度来说,zustand 就是 redux 的一个继任者。这一点可以从 vanilla create API 的实现可以看出。vanilla create API 的实现几乎是"抄"了 redux 的核心实现。zustand 同样实现了 store, middleware 等两个概念, 连 devtool 都是复用 redux 的 chrome devtool。不同的是,zustand 没有坚守 redux 的严格的单向数据流的设计中其他该概念,比如说:reducer, action, dispatch 等。然而,正如官网所指出的那样,你仍然可以把 zustand 写成 redux 的单向数据流的样子。
从使用层面来说,zustand 比 redux 范式更加的松散和灵活(unopinionated)。它没有那套像 redux 那样稍显得冗余的模板代码。简直可以说,怎么写怎么有。另外一个最大的不同是,zustand 可以在任何地方去调用 setState() 来触发 react 界面更新。这一点是 redux 做不到的。
另外的一个不同点是渲染性能方面的。zustand 从第一版本就实现了组件的按需渲染。而 redux 那边,在 redux toolkit 还没有推出之前,都是「全量刷新」的渲染方式。把 redux toolkit 看作加强版的 redux 的话,那么在「按需渲染」方面,zustand 和 redux 到目前为止,都是支持通过 selector 来实现这个功能了。
快速入门
正如上面「特点」小节所提到的那样,zustand 上手是很简单的 - 就是三部曲:
- 创建 store
- 在组件中使用
useBoundStore()来获取状态和 action 函数 - 在组件中调用 action 函数来触发状态更新
下面直接把官网的 demo 搬过来,整个三部曲就一步了然了:
- 创建 store
typescript
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
}))
- 在组件中使用
useBoundStore()来获取状态和 action 函数
typescript
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
//......
}
- 在组件中调用 action 函数来触发状态更新
typescript
function Controls() {
const increasePopulation = useBear((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
好了,到这就入门成功了。
原理揭秘
上面的简介只是一个热身活动,目的是让你对 zustand 的基本使用方法和特性有一个印象。下面我们来揭秘一下它的原理,以便于你更好地理解和使用 zustand。
源码框架
首先,我们看看源码的目录结构
bash
zustand/
├── src/
│ ├── index.ts # 默认入口:导出 create(React 版)
│ ├── vanilla.ts # 框架无关核心:createStore
│ ├── react.ts # React 绑定:useSyncExternalStore 封装
│ ├── traditional.ts # React 16/17 兼容入口(自动垫片)
│ ├── middleware/
│ │ ├── combine.ts
│ │ ├── devtools.ts
│ │ ├── immer.ts
│ │ ├── persist/
│ │ │ ├── index.ts
│ │ │ └── storage.ts
│ │ ├── redux.ts
│ │ └── subscribeWithSelector.ts
│ ├── vanilla/
│ │ ├── shallow.ts # 浅比较函数(供 vanilla 用户)
│ ├── react/
│ │ ├── shallow.ts # useShallow Hook
│ └── types.d.ts # 全局 TypeScript 类型定义
zustand 的核心代码采用了分层架构,目的是为了做到框架无关性。zustand 的核心代码分两层:
- vanilla 层 - 也就是原生层,是纯 javascript 实现,它不依赖任何的视图层框架。
- binding 层 - 也就是桥接特定的视图层框架。当前,这个视图层框架就是 react。
这两层之间的关联可以从下面的的鸟瞰图中看出:

简单来说,zustand 通过 react 原生 API useSyncExternalStore 把 react 内部的 listener(react core 在内部实现会创建一个 listenr) 注册到 zustand 原生 store 实例的 listener 数组中。如此一来,react 组件就成为了 zustand 原生 store 实例的众多订阅者中的一员。
当用户代码调用 store action 的时候,间接就调用了 store 实例的 setState 方法。而在 setState 方法的源码实现中,zustand 会遍历调用所有的 listener 函数。而这些 listener 函数中,就包括了 react 组件。这就是在通知 react 组件着手去判断组件是否真的需要更新。
在执行 react 注册到 store 实例的 listener 的时候,react 会对比前后两次的 snapshot 的值,如果值发生了变化, react 就会发起一个请强制更新的请求(forceStoreRerender(fiber))。那么,该 react 组件就会在下一轮的 render 阶段的时候得到 rerender。
限于篇幅,更多精彩内容请查看下篇《zustand 从原理到实践 - 原理篇(2)》。