大家好,我是风骨,本篇文章的主题是 React 状态库 - Zustand。
这是一个基于 React hooks API 实现的轻量级状态管理库。当你的项目不算太大,又期望使用全局状态,它将是一个不错的选择。
通过阅读本篇文章,你将收获:
- 掌握 zustand 的用法及使用注意事项 - 减少不必要的重渲染
- 掌握 React useSyncExternalStore hook 用途及工作原理
- 掌握 zustand 数据驱动组件更新的原理
附 - 毛遂自荐:笔者当前离职在看工作机会,各位小伙伴如果有合适的内推机会,期待您的引荐 🤝
个人简介:男,27岁,专科 · 计算机专业,6 年前端工作经历,从事过 AIGC、低代码、动态表单、前端基建 等方向工作,base 北京 - 高级前端岗,vx 联系方式:iamcegz
1、基本用法
- 首先,初始化项目和安装 zustand
我们使用 Vite cli 初始化一个简易的 React + TypeScript 项目,并安装 zustand 库。
md
# 使用 vite cli 创建项目
npm create vite@latest vite-zustand -- --template react-ts
# 安装 zustand 库
cd vite-zustand
npm install zustand
- 定义一个 store 状态仓库
新建 src/store/count.ts
文件,使用 zustand 提供的 create
函数创建一个用于 count 计数的 store:
tsx
// src/store/count.ts
import { create } from "zustand";
type State = {
count: number;
};
type Action = {
inc: () => void;
dec: () => void;
};
const useCountStore = create<State & Action>((set, get) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set(() => ({ count: get().count - 1 })),
}));
export default useCountStore;
create 函数使用介绍:
-
输入:它接收一个函数,
- 函数的参数是
set
和get
方法,set
方法用于更新值,get
方法用于获取值; - 函数的返回值是一个对象,在对象中定义
state
和action
,将会暴露给外部组件去使用;
- 函数的参数是
-
输出 :它返回一个
react hook
(代码中的 useCountStore),在函数组件可使用它来消费 store 中的 state 和 action。
- 在组件中消费 store
在函数组件中导入 useCountStore
,从中解构出 state 和 action,作用和 setState hook
相似。
tsx
// src/App.tsx
import useCountStore from "./store/count";
function App() {
const { count, inc, dec } = useCountStore();
return (
<div>
<h1>count is {count}</h1>
<button onClick={() => inc()}>inc</button>
<button onClick={() => dec()}>dec</button>
</div>
);
}
export default App;
到这里,我们成功在组件中接入了 zustand store,执行 npm run dev
启动项目操作页面上的按钮来体验一下。
- 在 store action 中执行异步操作
在定义 store 时,action 函数还支持书写 async/await 异步代码,比如在这里发起异步 API 请求拿到数据后再进行 state 更新。
PS:如果遵循纯函数原则,通常不建议在 action 中编写这些副作用(异步请求)操作。关于这一点小伙伴们有独特见解可以在评论区发起讨论。
定义一个异步函数 fetchCount
:
diff
// src/store/count.ts
import { create } from "zustand";
type State = {
count: number;
};
type Action = {
inc: () => void;
dec: () => void;
+ fetchCount: () => void;
};
const useCountStore = create<State & Action>((set, get) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set(() => ({ count: get().count - 1 })),
+ fetchCount: async () => {
+ // 模拟异步请求
+ const data = await new Promise<number>((resolve) => {
+ setTimeout(() => resolve(100), 1000);
+ });
+ set(() => ({ count: data }));
+ },
}));
export default useCountStore;
在组件中使用 fetchCount
:
tsx
// src/App.tsx
import useCountStore from "./store/count";
function App() {
const { count, inc, dec, fetchCount } = useCountStore();
return (
<div>
<h1>count is {count}</h1>
...
<button onClick={() => fetchCount()}>fetchCount</button>
</div>
);
}
export default App;
2、进阶用法
2.1、中间件
zustand 提供了 中间件机制 来扩展和增强操作 store 功能。比如 persist
中间件可以将 store state 持久化到 localStorage 中。
我们在 count store
中引入 persist
中间件,并采用 localStorage 存储形式定义一个 count-storage
存储名称:
tsx
// src/store/count.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
type State = {
count: number;
};
type Action = {
inc: () => void;
dec: () => void;
};
const useCountStore = create<State & Action>()(
persist(
(set, get) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set(() => ({ count: get().count - 1 })),
}),
{
name: "count-storage", // 存储的名称
storage: createJSONStorage(() => localStorage), // 存储的引擎: localStorage || sessionStorage
}
)
);
export default useCountStore;
persist
中间件会完成以下两件事情:
- 在更新 state count 时同时将最新数据存储在 localStorage
count-storage
中; - 在初始化 store 时会从本地存储中读取缓存,若读到则作为 state count 默认值。
有关其他中间件的使用,可阅读官方文档:zustand.docs.pmnd.rs/getting-sta...
2.2、减少不必要的组件重渲染
在使用 zustand 时,如果稍不注意,会导致一些 React 组件执行不必要的重渲染。让我们一起来看看!
现在我们有一个 store,存放了 主题 和 语言 两个 state。
tsx
// src/store/config.ts
import { create } from "zustand";
interface State {
theme: string;
lang: string;
}
interface Action {
setTheme: (theme: string) => void;
setLang: (lang: string) => void;
}
const useConfigStore = create<State & Action>((set) => ({
theme: "light",
lang: "zh-CN",
setLang: (lang: string) => set({ lang }),
setTheme: (theme: string) => set({ theme }),
}));
export default useConfigStore;
接着,我们创建 主题 和 语言 组件来消费对应的 state。
tsx
// src/components/Theme.tsx
import useConfigStore from "../store/config";
const Theme = () => {
const { theme, setTheme } = useConfigStore();
console.log("theme render");
return (
<div>
<div>{theme}</div>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
切换
</button>
</div>
);
};
export default Theme;
// src/components/Lang.tsx
import useConfigStore from "../store/config";
const Lang = () => {
const { lang, setLang } = useConfigStore();
console.log("lang render...");
return (
<div>
<div>{lang}</div>
<button onClick={() => setLang(lang === "zh-CN" ? "en-US" : "zh-CN")}>
切换
</button>
</div>
);
};
export default Lang;
同时,将这两个组件挂载到页面上。
tsx
import Theme from "./components/Theme";
import Lang from "./components/Lang";
function App() {
return (
<div>
<Theme />
<Lang />
</div>
);
}
export default App;
现在我们启动项目回到页面,你会发现:当去 setTheme 修改 主题 时,语言组件也会重渲染,同样当去 setLang 修改 语言 时,主题组件也会重渲染。
这是为什么呢?要搞懂这个需要我们阅读完下文「原理分析」
。
就目前,我们要优化这一问题,可以选用以下方式:
方式一:每个 useConfigStore 返回一个具体的 state。
tsx
const Theme = () => {
const theme = useConfigStore((state) => state.theme);
const setTheme = useConfigStore((state) => state.setTheme);
console.log("theme render");
...
};
缺点:书写乏味,需要编写很多重复的 useConfigStore。
方式二:返回一个仅包含需要的 state 的对象,注意需要使用官方提供的 Api -useShallow
进行对象浅比较,避免出现一些多次渲染导致栈溢出问题。
tsx
import { useShallow } from "zustand/react/shallow";
const Theme = () => {
const { theme, setTheme } = useConfigStore(
useShallow((state) => ({
theme: state.theme,
setTheme: state.setTheme,
}))
);
console.log("theme render");
...
};
3、原理分析
从使用上我们知道 zustand
主要有两个关键的函数来为组件提供状态:
- 一是
create
函数,用于定义state
和action
, - 二是
create
函数返回值useBoundStore
,用于桥接 React 组件和 store 数据。
3.1、create 函数
源码:
create
函数接收一个 createState
函数,创建了一个 storeApi
,并返回一个 useBoundStore
函数。
tsx
const create = (createState) => {
const storeApi = createStore(createState);
const useBoundStore = (selector) => useStore(storeApi, selector);
Object.assign(useBoundStore, storeApi);
return useBoundStore;
};
分析:
createStore
函数接收用户定义的createState
函数,执行并拿到state
及action
,同时对外暴露一些用于操作store
的api
。源码如下:
tsx
const createStore = (createState) => {
let state;
const listeners = new Set(); // 观察者模式的监听集合
// 定义更新 state 方法
const setState = (partial, replace) => {
const nextState = typeof partial === "function" ? partial(state) : partial;
// 使用 Object.is 比较两个对象是否是同一个引用地址,等同于使用全等 ===
if (!Object.is(nextState, state)) {
const previousState = state;
// 更新 state,如果 replace 为 true,则直接替换,否则浅合并到 state 上
state = repeat ? nextState : Object.assign({}, state, nextState);
// notify 通知
listeners.forEach((listener) => listener(state, previousState));
}
};
// 定义获取 state 方法
const getState = () => state;
const getInitialState = () => initialState;
// 观察者订阅函数,将 listener 添加到 listeners 集合中,并返回一个取消订阅的函数
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
// 操作 store 的 api
const api = { setState, getState, getInitialState, subscribe };
// 执行 createState 函数初始化 state,并赋值给 initialState 和 state
const initialState = (state = createState(setState, getState, api));
return api;
};
storeApi
是一个包含操作 store 的对象,包含以下方法:subscribe
方法用于订阅观察者到listeners
集合中,属于观察者模式思想,将来在更新数据的时候通知这些观察者;setState
用于更新state
,同时通知listeners
集合中的每一个观察者;getState
则是获取最新的state
。
tsx
storeApi = { setState, getState, getInitialState, subscribe };
useBoundStore
它接受一个用户传入的selector
函数,交给useStore
函数进行处理,下文会详细介绍;
tsx
const useBoundStore = (selector) => useStore(storeApi, selector);
- 将
storeApi
合并作为useBoundStore
函数的成员属性,这意味着可以从useBoundStore
中获取到storeApi
(不过实际的应用场景应该不多):
tsx
useBoundStore.getState()
useBoundStore.setState(...)
小结:
create
函数定义了 state
及更新和获取 state
的方法,但还有两个疑问需要我们去探索:
useStore
如何处理selector
并返回用户需要的store
数据?subscribe
在何时使用,来向listeners
集合中添加观察者?
useStore
的实现会帮助我们解答疑惑!
3.2、useStore 函数
在 React 组件中 消费 store 会用到 useBoundStore
(即 useCountStore
),
tsx
import useCountStore from "./store/count";
function App() {
const { count, inc, dec } = useCountStore();
...
}
useBoundStore
本质是调用 useStore
并传递一个 selector
函数选择要读取哪些 store 状态,最后来返回 store 状态,我们来看看 useStore 的实现。
源码:
tsx
const identity = (arg) => arg;
function useStore(api, selector = identity) {
const slice = React.useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
);
return slice;
}
看起来实现很简单,使用了 React.useSyncExternalStore
(直译为:同步外部 store 作为 state 数据源)。
如果你不了解 React.useSyncExternalStore
的作用,让我们一起来学习一下!
3.3、useSyncExternalStore hook
传统的组件状态 state 需要使用 useState、useReducer 等 API 定义,这样在数据更新时才能驱动组件重渲染。
useSyncExternalStore
是 React18 新增的一个 hook,它的作用是:将外部的一个普通对象 state 接入到组件内,在数据更新时也能驱动组件重渲染。
再回过来看 zustand,它的 state 数据定义就是一个普通对象,然而借助这个 API 就可以将 state 和 React 组件桥接在一起,完美实现:更新 state --> 驱动 React 组件 rerender。
3.3.1、用法
首先我们定义一个 state
普通对象,并配套实现一个观察者模式 listeners
:
ts
let state = {
x: 0,
y: 0,
};
const listeners: (() => void)[] = [];
function subscribe(callback: () => void) {
listeners.push(callback);
return () => {
listeners.splice(listeners.indexOf(callback), 1);
};
}
接着,我们在 React 组件中使用 useSyncExternalStore
hook 接入这个 state
及其 listeners
:
tsx
function App() {
const { x, y } = React.useSyncExternalStore(subscribe, () => state);
return (
<button onClick={() => setState({ x: x + 1 })}>更新 x: {x}</button>
)
}
- 参数一:一个用于注册观察者的
subscribe
方法,来监听 state 发生变化,自行驱动 App 组件重渲染; - 参数二:是一个函数,定义要返回的 state 值。
现在,我们定义一个 setState 方法,在更新 state 的同时,通知观察者数据更新了。
tsx
const setState = (partial: Partial<typeof state>) => {
state = { ...state, ...partial };
listeners.forEach((listener) => listener());
};
回到页面,点击按钮,state.x 的值不断更新,同时组件也会进行重渲染,完美在组件中接入外部数据。
3.3.2、原理
我们思考一下 useSyncExternalStore
的核心是什么呢?
答案是 subscribe
观察者。
在观察到数据更新的时候,它执行了类似 forceUpdate
的组件重渲染操作,调用底层的 scheduleUpdateOnFiber
方法开启更新流程,从而实现数据驱动视图更新。
useSyncExternalStore
简易版实现(以 mount 为例):
tsx
function mountSyncExternalStore(subscribe, getSnapshot) {
const fiber = currentlyRenderingFiber;
const hook = mountWorkInProgressHook();
const nextSnapshot = getSnapshot(); // 获取 state 的快照(值)
hook.memoizedState = nextSnapshot;
const inst = {
value: nextSnapshot,
getSnapshot,
};
hook.queue = inst;
// !!! 在 useEffect 中执行 subscribe 来订阅 state
mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
return nextSnapshot; // 返回 state
}
// !!! 关键方法
function subscribeToStore(fiber, inst, subscribe) {
// 在 store 数据发生变化时,执行 forceStoreRerender 方法,开启强制渲染更新流程
const handleStoreChange = () => {
if (checkIfSnapshotChanged(inst)) {
forceStoreRerender(fiber);
}
};
// Subscribe to the store and return a clean-up function.
return subscribe(handleStoreChange);
}
// 检测前后 state 是否发生变化
function checkIfSnapshotChanged(inst) {
const latestGetSnapshot = inst.getSnapshot;
const prevValue = inst.value;
try {
const nextValue = latestGetSnapshot();
return !Object.is(prevValue, nextValue);
} catch (error) {
return true;
}
}
// 开启强制渲染更新流程
function forceStoreRerender(fiber) {
// 标记 fiber lane 更新为 SyncLane 同步优先级
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
// 开启调度更新
scheduleUpdateOnFiber(root, fiber, SyncLane);
}
}
核心是 subscribeToStore()
,在 subscribe
订阅到外部 store 数据更新后,执行 forceStoreRerender(fiber)
开启新的更新流程。
同时,在这里我们解答一下上面遗留的疑问:「为什么 setTheme 修改主题,语言组件会重渲染」
。
- 在没有传递
selector
时,(arg) => arg
会作为默认值,即将 store state 返回; - 从 store api setState 的实现可以得知,每次更新都会创建一个全新对象 {};
- 重点:因为每次更新都是全新对象,在
checkIfSnapshotChanged()
中通过Object.is()
比较前后 store state 发现引用地址不一样 ,因此语言组件也会触发scheduleUpdateOnFiber
并在组件上标记上更新标识。 - 因此解决办法便是:使用官方提供的 Api -
useShallow
进行对象浅比较 。若语言组件依赖的 lang state 没有更新,在checkIfSnapshotChanged()
这一层便不会触发更新流程。
3.4、流程图
总结:通过「观察者模式」,在 state 发生更新时,只会通知消费该 state 的组件进行重渲染,实现了按需更新,从而确保高效的性能。
下面绘制了一版简易流程图,辅助大家进行理解:

文末
感谢阅读。文章内容你觉得有用,可以点赞支持一下~
参考:
关于zustand的一些最佳实践\] [juejin.cn/post/731679...](https://juejin.cn/post/7316796505129091081 "https://juejin.cn/post/7316796505129091081")