Zustand 简介
Zustand 是一个轻量级的状态管理库,适用于 React 应用。它提供简单的 API 和高效的性能,适合中小型项目或需要替代 Redux 的场景。Zustand 的核心特点是基于 Hook 的设计,无需复杂的 Provider 嵌套。
核心特性
- 极简 API :通过
create函数创建 store,直接使用 Hook 访问状态。 - 无需 Provider:状态全局共享,避免组件树嵌套。
- 高性能:自动优化渲染,仅更新依赖变更的组件。
- 中间件支持:可扩展日志、持久化等功能。
安装
bash
npm install zustand
基本用法
创建 Store
通过 create 方法定义 store,包含状态和更新逻辑:
javascript
import { create } from 'zustand';
interface StoreType {
count: number;
increment: () => void
}
const useStore = create<StoreType>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
在组件中使用
直接通过 Hook 访问状态或动作:
javascript
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
进阶功能
immer
immer中间件通常用于状态管理库(如Redux),允许以
不可变的方式直接修改状态。它利用Proxy或Object.defineProperty实现"草稿状态"的修改,最终返回新的不可变状态,简化了深层嵌套数据的更新逻辑。
核心功能
- 不可变性:自动处理状态的不可变更新,避免手动展开(spread)或
深拷贝。 - 语法简化:支持直接修改"草稿"对象,如draft.value = 1,内部转换为不可变更新。
- 性能优化:
仅修改变化的部分,未修改的数据保持引用不变。
安装
bash
npm install immer
React 用法
需要导出produce,然后它的第一个参数是原始值,第二个参数是一个回调函数,回调函数中的参数是draft,也就是原始值的拷贝,然后我们就可以直接修改draft了,最后返回新的值
ts
// 1. 纯 Immer,与任何状态库无关
import { produce } from 'immer';
const oldState = {
user: { name: '张三', age: 18 },
roles: ['editor']
};
// 更新函数
const nextState = produce(oldState, draft => {
draft.user.age = 20; // 想怎么改就怎么改
draft.roles.push('admin'); // 不需要手写展开符
});
console.log(oldState === nextState); // false( immutable )
console.log(nextState.user.age); // 20
Zustand用法
ts
// store/useTodoStore.ts
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer'; // 记住这里的导入是从zustand里导入的
type Todo = { id: number; title: string; done: boolean };
type Store = {
todos: Todo[];
addTodo: (title: string) => void;
toggleTodo: (id: number) => void;
};
export const useTodoStore = create(
immer<Store>((set, get) => ({
todos: [],
addTodo: title =>
set(state => {
state.todos.push({ id: Date.now(), title, done: false }); // 直接 push!
}),
toggleTodo: id =>
set(state => {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done; // 直接改属性!
}),
}))
);
用法的对比
| 场景 | 写法 | 是否必须 Immer |
|---|---|---|
React 本地 useState |
setState(produce(draft => { ... })) |
可选,但省代码 |
| 全局轻量 store | create(() => ({ ... })) |
不需要 |
| 全局 深层 store | create(immer(() => ({ ... }))) |
强烈推荐 |
Immer 的核心原理一句话概括:
"用 Proxy 把你对草稿的每一次'非法'修改都拦截下来,然后只复制被触碰的节点,最终拼出一棵全新的不可变树,其余部分保持引用不变。"
useShallow
useShallow是 Zustand 官方提供的 React Hook 包装器,专门解决"一次选多个状态"时因"每次返回新对象"而导致的 多余重渲染 问题。
它内部做了一件很简单的事:把 selector 的结果做一次浅比较(shallow equal),如果顶层字段都一样,就强制跳过本次渲染。
用法
ts
import { useShallow } from 'zustand/react/shallow' // 推荐,React 版
// or
import { useShallow } from 'zustand/shallow' // 纯函数版,也能用
小栗子
ts
// store.ts
// 1. 状态
interface State {
count: number;
name: string;
}
// 2. 方法(Actions)
interface Actions {
inc: () => void;
setName: (n: string) => void;
}
// 3. 合并后给组件用的类型(可选)
export type StoreType = State & Actions;
export const useCounter = create<StoreType>(() => ({
count: 0,
name: 'Counter',
inc: () => useCounter.setState(s => ({ count: s.count + 1 })),
setName: (n: string) => useCounter.setState({ name: n })
}))
// Component.tsx
import { useShallow } from 'zustand/react/shallow'
function Counter() {
// 只会在 count 或 inc 变化时才渲染
const { count, inc } = useCounter(
useShallow(state => ({
count: state .count,
inc: state .inc
}))
)
return (
<>
<p>{count}</p>
<button onClick={inc}>+1</button>
</>
)
}
注意:
- 只浅比较一层
如果 selector 返回嵌套对象 { user: state.user },user.id 变化 不会被检测到;此时应把需要的数据平铺到顶层。 - 不要在外层包 useMemo
useShallow 内部已经做了 memo,再包 useMemo 反而增加复杂度。 - 和 persist/immer 不冲突
可以任意组合:create(persist(immer(...))) 后再 useShallow 选状态即可。
中间件
zustand 的中间件是用于在状态管理过程中添加额外逻辑的工具。它们可以用于日志记录、性能监控、数据持久化、异步操作等。在 Zustand 里,"中间件"不是 Express 那种"洋葱圈"模型,而是 "高阶函数":
config => newConfig,把原始的 set / get / store 包一层,先拦截、再决定是否往下传。
| 名称 | 一句话作用 | 安装 |
|---|---|---|
| immer | 让你直接改 state 却生成不可变值 |
zustand/middleware/immer |
| persist | 把状态同步到 localStorage / IndexedDB |
zustand/middleware/persist |
| devtools | 接入 Redux DevTools 时间旅行 | zustand/middleware/devtools |
| subscribeWithSelector | 给 store.subscribe 加 selector + 相等比较 |
zustand/middleware/subscribeWithSelector |
| combine | 快速把"状态"和"动作"拼在一起(少写一次类型) | zustand/middleware/combine |
immer上文已经介绍
persist(数据固化)
ts
import { persist, createJSONStorage } from 'zustand/middleware'
export const useStore = create<Store>()(
persist(
(set, get) => ({
count: 0,
name: '张三',
inc: () => set({ count: get().count + 1 })
}),
{
name: 'counter-storage', // localStorage key(唯一)
storage: createJSONStorage(() => localStorage), // sessionStorage、localStorage
// 可以选择需要存储的数据
partialize: (s) => ({ count: s.count }) // 只持久化 count
}
)
)
注:persist另外提供了一个清楚存储的方法clearStorage
ts
export default function UseStore() {
console.log("Store组件渲染~");
// const { price, incrementPrice, decrementPrice, resetPrice } = usePriceStore();
const {price, incrementPrice, decrementPrice, resetPrice } = usePriceStore(useShallow((state) => ({
price: state.price,
incrementPrice: state.incrementPrice,
decrementPrice: state.decrementPrice,
resetPrice: state.resetPrice
})))
const { gourd, updateGourd } = useUserStore();
function getUser() {
updateGourd();
console.log(gourd);
}
return (
<div>
<Flex vertical className="card">
<h1>测试Zutand</h1>
<div>Pirce {price}</div>
<Flex vertical gap={"small"}>
<Flex justify="space-between" gap={"small"}>
<Button type="primary" style={{ flex: 1 }} onClick={incrementPrice}>加加</Button>
<Button type="primary" style={{ flex: 1 }} onClick={decrementPrice}>减减</Button>
</Flex>
<Button type="primary" danger onClick={resetPrice}>重置</Button>
<Button type="primary" danger onClick={usePriceStore.persist.clearStorage}>重置缓存</Button>
<Button type="primary" danger onClick={getUser}>升级</Button>
</Flex>
</Flex>
</div>
)
}
devtools(调试)
搭配Redux DevTools调试更简单方便。

ts
import { devtools } from 'zustand/middleware'
export const useStore = create<Store>()(
devtools(
(set, get) => ({
count: 0,
inc: () => set({ count: get().count + 1 }, false, 'inc') // 第三个参数=action type
}),
{
name: 'price-store', // store名称
enabled: import.meta.env.DEV // 是否开启检测
}
)
)

多中间件组合顺序
ts
create<Store>()(
devtools(
immer(
persist((set, get) => ({ ... }), { name: 'key' })
)
)
)
自定义中间件
ts
const logMiddleware: Middleware<Store> = (config) => (set, get, api) =>
config(
(...args) => {
console.log('prev', get())
set(...args)
console.log('next', get())
},
get,
api
)
export const useStore = create<Store>()(
logMiddleware((set, get) => ({
count: 0,
inc: () => set({ count: get().count + 1 })
}))
)
订阅
zustand 的 subscribe,可以订阅一个状态,当状态变化时,会触发回调函数,可以类比watch监听函数。
最简API
ts
const unsub = store.subscribe(
(state) => state.count, // selector,可选
(count, prevCount) => {
// 变化时触发 count(当前值), prevCount(之前值)
count++
},
{ fireImmediately: true } // 选项,可选
)
// 取消监听
unsub()
- 如果不传 selector,默认监听整个 state(引用变化即触发)。
ts
const unsub = store.subscribe(
state => {
/* 变化时触发 */
state.count++
},
{ fireImmediately: true } // 订阅瞬间先跑一次(可选)
)
- 返回的函数调用后即可取消订阅。
两种用法
| 场景 | 拿到 store 的方式 |
|---|---|
| 在组件外(如 utils、事件总线) | import { useStore } from './store'; const store = useStore.getState() |
| 在组件内(useEffect 里) | const store = useStore 本身就是 store 对象 |
ts
const store = create((set) => ({
count: 0,
}));
//外部订阅
store.subscribe((state) => {
console.log(state.count);
});
//组件内部订阅
useEffect(() => {
store.subscribe((state) => {
console.log(state.count);
});
}, []);
小栗子(组件外打印日志)
ts
// store.ts
export const useCounter = create(() => ({
count: 0,
inc: () => useCounter.setState(s => ({ count: s.count + 1 }))
}))
// logger.ts(纯 JS 文件)
const unsub = useCounter.subscribe(
state => state.count,
(count, prevCount) => {
console.log('[logger] count', prevCount, '→', count)
}
)
// 之后任何代码调用 useCounter.getState().inc() 都会触发上面日志
与 React useEffect 结合
ts
function CountLabel() {
useEffect(() => {
const unsub = useCounter.subscribe(
state => state.count,
cnt => {
document.title = `Counter: ${cnt}` // 副作用:改标题
}
)
return unsub // 组件卸载时取消
}, [])
return <span>{useCounter(s => s.count)}</span>
}
订阅整个 state(无 selector)
ts
useCounter.subscribe(
state => state, // 不写也行,默认就是 identity
(state, prevState) => {
if (state.count !== prevState.count) {
// 自己手动 diff
}
}
)
subscribeWithSelector 中间件(可选)
如果你希望 selector 返回值做浅比较(而不是引用比较),装这个中间件:
ts
import { subscribeWithSelector } from 'zustand/middleware'
const useStore = create(
subscribeWithSelector(() => ({ count: 0, name: '' }))
)
// 现在 subscribe 的 selector 默认浅比较
useStore.subscribe(
state => ({ cnt: state.count }), // 返回对象也不会疯狂触发
({ cnt }) => console.log(cnt)
)
常见误区
| 误区 | 正确理解 |
|---|---|
subscribe 会触发 React 重新渲染 |
❌ 它只跑回调 ,与渲染无关;渲染用 useStore(selector) |
| 必须在组件里才能用 | ❌ 任何地方 都能 store.subscribe |
| 返回的新 state 是深拷贝 | ❌ 只是不可变新引用,内部结构共享 |