手把手带你掌握Zustand:轻量级React状态管理利器

一、简介
一个小巧、快速且可扩展的极简状态管理解决方案。Zustand 提供了一个基于 Hooks 的舒适 API,它不啰嗦、不强加架构约束,但有足够的约定性,使其语义明确,风格类似 Flux。
Zustand(在德语中意为"状态")是一个轻量、快速、可扩展的 React 状态管理库。它基于 Hooks API,并且非常灵活,不需要像 Redux 那样定义复杂的 Action、Reducer、Store 组合等模板代码,却仍然保留了单向数据流、不可变数据等良好的状态管理理念。
1. 特点
- 基于不可变对象:使用不可变数据有助于调试、性能优化以及实现时间旅行等功能。
- 单向数据流:保证数据流动可预测,逻辑更清晰。
- 简洁的 API:只需要通过 create 方法创建 store,通过返回的 Hooks 进行状态读取和更新。
- 无需 React Context:无需使用 包裹根组件,也能在任意组件中使用 Zustand 的 Store(类组件里也可通过其他方式获取)。
- 无痛上手:对 Redux 原理熟悉的同学,上手几乎零门槛;初学者也能快速掌握。
2. 核心优势
- 🚀 极简API:仅需掌握create、setState、getState三个核心方法
- 🧩 零依赖:单文件体积仅1kB,无需额外配置
- ⚡ 高性能:自动依赖追踪,精准更新组件
- 🧠 直观开发:类Redux单向数据流,但无繁琐模板代码
- 🌐 灵活扩展:支持中间件生态,轻松对接调试工具
3. 适用场景
- ✅ SPA应用开发
- ✅ 中小型项目
- ✅ 需要快速迭代的场景
- ✅ 开发者希望简化状态管理
二、Zustand 的使用
我们以一个计数器(Counter)示例入手,演示三件最基础的事情:
- 创建一个 store 并定义状态(state)及更新它的函数(action)。
- 在组件中读取 store 中的状态。
- 触发操作时更新状态,从而让 UI 重新渲染。
下面的示例演示了一个简单计数器的用法:
typescript
import React from 'react';
import styles from './Counter.module.css';
import { create } from 'zustand';
/**
* 1. 通过 create 函数创建一个 store
*/
const useCountStore = create((set, get, api) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
}));
export function Counter() {
/**
* 2. 通过返回的 Hooks + selector 读取想要的状态或操作函数
*/
const value = useCountStore((state) => state.value);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={decrement}
>
-
</button>
<span className={styles.value}>{value}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={increment}
>
+
</button>
</div>
</div>
);
}
运行后,可以点击加减按钮实时更新计数,使用非常直观。Zustand 用 create 创建的 store,默认会返回一个自定义的 Hook(如上例的 useCountStore),它可以在任何函数组件里直接调用来读写状态。
三、核心实现原理
Zustand 核心实现基于以下几点:
1. 创建 Store
通过 createState 函数初始化状态,并返回一个 api 对象,包含 setState、getState、subscribe、destroy 等方法。整体就是一个简易的「发布-订阅」模型:
- setState:更新状态并通知所有订阅者。
- getState:获取当前状态。
- subscribe:订阅状态更新,当 state 改变时被触发。
- destroy:销毁或清空所有监听器。
2. 在组件中通过 Hooks 订阅更新
返回的 useBoundStore 函数,内部使用 useSyncExternalStoreWithSelector 来订阅 store 的更新,并根据 selector 计算出具体想要的状态切片。当这个切片发生变化时,会触发组件重新渲染。
3. 选择器(selector)与比较函数(equalityFn)
- selector:只选择所需的局部数据,有助于减少不必要的组件重渲染。
- equalityFn:自定义对比逻辑,默认使用 Object.is。如果前后两次 selector 计算出的结果"相等",则避免触发渲染。
四、核心代码示例
下面是一个经过简化的核心实现示例:
typescript
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector';
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
/** 1. 创建 store 并返回 api */
function createStore(createState) {
let state;
const listeners = new Set();
const setState = (partial, replace) => {
const nextState = typeof partial === 'function' ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state =
replace ?? typeof nextState !== 'object'
? nextState
: Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
const getState = () => state;
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const destroy = () => listeners.clear();
const api = { setState, getState, subscribe, destroy };
// 执行 createState 函数进行初始化
state = createState(setState, getState, api);
return api;
}
/** 2. 在 Hooks 中使用 subscribe/getState */
function useStore(api, selector, equalityFn) {
return useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState || api.getState,
selector,
equalityFn
);
}
/** 3. create 函数:将 store 和 useStore 结合起来 */
export function create(createState) {
const api = createStore(createState);
const useBoundStore = (selector, equalityFn) =>
useStore(api, selector, equalityFn);
Object.assign(useBoundStore, api);
return useBoundStore;
}
从上面可以看出,Zustand 其实就是一个单例式的状态存储 + Hooks 订阅更新的组合。
五、Zustand 中间件
Zustand 提供了各种中间件(middleware),用来增强 Store 的功能。例如配合 immer 可以更方便地写不可变数据;或者使用 devtools 中间件来与 Redux DevTools 进行调试。
例如,使用 immer 中间件的示例:
tsx
import React from 'react';
import styles from './Counter.module.css';
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
const useCountStore = create(
immer((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
// 示例:管理一个 todos 列表
todos: [
{ id: '1', title: 'Learn Zustand', done: false },
{ id: '2', title: 'Learn Jotai', done: false },
{ id: '3', title: 'Learn Valtio', done: false },
],
complete: (todoId) => {
set((state) => {
const todo = state.todos.find((t) => t.id === todoId);
if (todo) todo.done = true; // Immer 会自动帮我们处理不可变数据
});
},
}))
);
export function Counter() {
const value = useCountStore((state) => state.value);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
const todos = useCountStore((state) => state.todos);
const complete = useCountStore((state) => state.complete);
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={decrement}
>
-
</button>
<span className={styles.value}>{value}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={increment}
>
+
</button>
</div>
<div className={styles.row}>
<ul>
{todos.map(({ id, title, done }) => (
<li
className={`${styles.todoItem} ${done ? styles.done : ''}`}
key={id}
>
{title}
<button
className={styles.todoButton}
onClick={() => complete(id)}
>
✔️
</button>
</li>
))}
</ul>
</div>
</div>
);
}
这种写法可以用"可变"的方式直接修改状态,真正保存时则自动帮你处理不可变数据,开发体验更佳。
六、自定义中间件
Zustand 中间件没有像 Redux 那样固定的形式和层级,而是一个「增强器」:本质上就是对传入的 createState 做一层包装,仍然需要返回一个新的 createState 函数。可以随时组合或嵌套多个中间件(洋葱模型)。
下面以一个「日志中间件」(logger) 为例,每次 state 更新时都在控制台打印前后状态信息:
tsx
import React from 'react';
import styles from './Counter.module.css';
import { create } from 'zustand';
const logger = (createState) => (set, get, api) =>
createState((...args) => {
console.group('setState');
console.info('before:', get());
set(...args);
console.info('after: ', get());
console.groupEnd();
}, get, api);
const useCountStore = create(
logger((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
}))
);
export function Counter() {
const value = useCountStore((state) => state.value);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
return (
<div>
<div className={styles.row}>
<button className={styles.button} onClick={decrement}>
-
</button>
<span className={styles.value}>{value}</span>
<button className={styles.button} onClick={increment}>
+
</button>
</div>
</div>
);
}
想要再叠加更多中间件时,只要多层函数嵌套即可。例如:
tsx
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
// ...
const useCountStore = create(
immer(
logger(
devtools((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
}))
)
)
);
其调用顺序是:immer => logger => devtools,然后执行到真实的 setState。
七、与 Redux/Redux Toolkit 的对比
如果只看最基础的 Redux,你会觉得要写大量模板代码:Action、Reducer、combineReducers...... 通常实际项目会使用 Redux Toolkit 来简化这一切,并内置 immer、reselect 等实用库。
下表对比了 Redux Toolkit 与 Zustand 的主要差异(这里只列举一些核心思路):
对比点 | Zustand | Redux Toolkit |
---|---|---|
单向数据流 | 是 | 是 |
不可变数据 | 是 (可手动或使用 immer middleware) | 是 (内置 immer) |
React Hooks 支持 | 原生支持,无需 Context | 官方提供 react-redux Hooks |
是否需要 Provider | 否(也可配合 Context,但并非必须) | 通常需要 |
学习曲线与概念 | 简洁明了,API 少,无需额外处理异步 | 有较多概念:store, slice, thunk, saga 等。Redux Toolkit 虽然简化了流程,但整体概念依然相对多元。 |
处理副作用 | 可直接在 action 函数里写异步逻辑 | reducer 必须保持纯函数,一般会配合 redux-thunk 或 redux-saga 等中间件处理异步 |
特点与社区 | 轻量、易上手,体积小;灵活性高,不强制统一的 action 派发模式;社区生态也在不断发展中 | 社区成熟,配套中间件与工具链完备;支持 devtools 与时间旅行调试;大型应用有更丰富完善的第三方生态 |
简单来说:Zustand 更适合快速开发或中小型项目,或者你想让状态管理更"松耦合"一些;而 Redux Toolkit 则拥有最成熟的生态、Devtools 支持、大规模项目实践经验,也让数据流更加可追踪、可调试。
如果你需要埋点或对动作(Action)逐条记录,Redux 拥有天然的派发机制可以在中间件轻松拦截;而在 Zustand 里,你需要在每个更新函数里手动埋点。
八、总结
- Zustand 的核心思路:在一个闭包里保存应用状态,通过发布订阅模式和 useSyncExternalStore(带 Selector)管理组件更新。对比传统 Redux,它省去了定义动作与拆分 Reducer 等冗长过程,但仍然保有单向数据流和不可变数据所带来的可维护性和可预测性。
- 可插拔中间件:Zustand 中间件不拘泥于某种形式,易于自定义。你可以轻松编写 Logger、权限校验或数据持久化等中间件,并与其他官方中间件(如 devtools, immer)堆叠使用。
结语
Zustand 以其"极简却不失灵活"的特性,成为了许多开发者在 React 项目中管理状态的首选。它的用法和原理都相当直观,并且拥有良好的性能表现。对于想快速开发、并希望在未来随时可扩展的项目而言,Zustand 不失为一种优秀的选择。
祝你写码愉快,保持好"Zustand"~(好状态)!