Zustand 完全指南
Zustand 是 React 生态中一个轻量、无样板的状态管理库,基于 hooks,不依赖 Context Provider。 使用 create 函数定义 store,接收一个返回状态和操作的工厂函数: 通过选择器(selector)订阅 store 的特定字段,避免无关字段变化导致的重渲染: Zustand v4+ 推荐将泛型写在 create<T>() 上: 将 store 状态持久化到 localStorage 或其他存储: persist 配置项: 配合 immer 中间件,可以使用直接修改 state 的写法(内部仍为不可变更新): 默认的
官方文档:https://docs.pmnd.rs/zustand/getting-started/introduction
适用版本:Zustand 5.x(2026-05-07 核实)
Zustand 是 React 生态中一个轻量、无样板的状态管理库,基于 hooks,不依赖 Context Provider。
安装
npm install zustand
基础用法
创建 Store
使用 create 函数定义 store,接收一个返回状态和操作的工厂函数:
import { create } from 'zustand'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
set 函数的两种用法
| 用法 | 说明 |
|---|---|
set({ key: value }) |
对象形式,与现有 state 浅合并 |
set(state => ({ key: value })) |
函数形式,接收当前 state,返回要合并的部分 |
set({ ... }, true) |
第二个参数为 true 时,完全替换 state(不合并) |
// 浅合并:只更新 count,其他字段不变
set({ count: 5 })
// 函数形式:基于当前值计算
set((state) => ({ count: state.count + 1 }))
// 完全替换(慎用,会删除所有其他字段)
set({ count: 0 }, true)
在 action 中使用 get 读取当前 state
import { create } from 'zustand'
interface CartStore {
items: string[]
addItem: (item: string) => void
removeItem: (item: string) => void
total: () => number
}
const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => {
// 通过 get() 获取最新 state,适合在需要读取后再写入的场景
const current = get().items
if (!current.includes(item)) {
set({ items: [...current, item] })
}
},
removeItem: (item) => {
set((state) => ({
items: state.items.filter((i) => i !== item)
}))
},
// 派生计算也可以作为函数放在 store 内
total: () => get().items.length
}))
在 React 组件中使用
通过选择器(selector)订阅 store 的特定字段,避免无关字段变化导致的重渲染:
import React from 'react'
function Counter() {
// 只订阅 count 字段,其他字段变化不会触发重渲染
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
const reset = useCounterStore((state) => state.reset)
return (
<div>
<span>{count}</span>
<button onClick={increment}>+1</button>
<button onClick={reset}>重置</button>
</div>
)
}
TypeScript 支持
类型定义方式
Zustand v4+ 推荐将泛型写在 create<T>() 上:
import { create } from 'zustand'
// 分离接口定义,便于复用和测试
interface UserState {
user: User | null
isLoading: boolean
}
interface UserActions {
fetchUser: (id: number) => Promise<void>
clearUser: () => void
}
type UserStore = UserState & UserActions
const useUserStore = create<UserStore>((set) => ({
user: null,
isLoading: false,
fetchUser: async (id) => {
set({ isLoading: true })
try {
const user = await api.getUser(id)
set({ user, isLoading: false })
} catch {
set({ isLoading: false })
}
},
clearUser: () => set({ user: null })
}))
中间件
persist:持久化
将 store 状态持久化到 localStorage 或其他存储:
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'light',
language: 'zh-CN',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language })
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage)
}
)
)
persist 配置项:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string |
必填 | localStorage 的 key 名称 |
storage |
PersistStorage |
localStorage |
存储实现,可替换为 sessionStorage 或自定义 |
partialize |
(state) => Partial<State> |
保存全部 | 只持久化部分字段 |
version |
number |
0 |
数据版本号,版本不匹配时触发 migrate |
migrate |
(persistedState, version) => State |
undefined |
版本迁移函数 |
merge |
(persistedState, currentState) => State |
浅合并 | 自定义合并逻辑 |
skipHydration |
boolean |
false |
跳过初始水合(SSR 场景) |
persist(
(set) => ({ ... }),
{
name: 'auth-storage',
// 只持久化 token,不持久化临时状态
partialize: (state) => ({ token: state.token }),
// 版本迁移示例
version: 1,
migrate: (persistedState: any, version) => {
if (version === 0) {
// 从旧格式迁移
return { token: persistedState.authToken }
}
return persistedState
}
}
)
immer:支持 mutation 写法
配合 immer 中间件,可以使用直接修改 state 的写法(内部仍为不可变更新):
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface TreeStore {
nodes: Record<string, { name: string; children: string[] }>
addChild: (parentId: string, childId: string) => void
}
const useTreeStore = create<TreeStore>()(
immer((set) => ({
nodes: {},
addChild: (parentId, childId) => {
set((state) => {
// 可以直接 mutation,immer 会转换为不可变操作
state.nodes[parentId]?.children.push(childId)
})
}
}))
)
devtools:Redux DevTools 集成
import { devtools } from 'zustand/middleware'
const useStore = create<Store>()(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment')
// ^ action 名称,在 DevTools 中显示
}),
{ name: 'CounterStore' } // DevTools 中显示的 store 名称
)
)
subscribeWithSelector:细粒度订阅
默认的 subscribe 在任何字段变化时都会触发,subscribeWithSelector 允许只订阅特定字段:
import { subscribeWithSelector } from 'zustand/middleware'
const useStore = create<Store>()(
subscribeWithSelector((set) => ({
count: 0,
name: 'default',
increment: () => set((state) => ({ count: state.count + 1 }))
}))
)
// 只监听 count 变化,name 变化不会触发
const unsubscribe = useStore.subscribe(
(state) => state.count,
(count, prevCount) => {
console.log(`count changed: ${prevCount} -> ${count}`)
}
)
// 取消订阅
unsubscribe()
组合多个中间件
const useStore = create<Store>()(
devtools(
persist(
immer((set) => ({
// ...
})),
{ name: 'my-store' }
),
{ name: 'MyStore' }
)
)
高级模式
切片(Slice)模式
当 store 较大时,按功能拆分为多个 slice 再组合:
import { create, StateCreator } from 'zustand'
// 定义 bear slice
interface BearSlice {
bears: number
addBear: () => void
}
const createBearSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
BearSlice
> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 }))
})
// 定义 fish slice
interface FishSlice {
fishes: number
addFish: () => void
}
const createFishSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
FishSlice
> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 }))
})
// 组合 store
const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a)
}))
异步 action
Zustand 的 action 本身就是普通函数,直接使用 async/await 即可:
interface PostStore {
posts: Post[]
isLoading: boolean
error: string | null
fetchPosts: () => Promise<void>
}
const usePostStore = create<PostStore>((set) => ({
posts: [],
isLoading: false,
error: null,
fetchPosts: async () => {
set({ isLoading: true, error: null })
try {
const response = await fetch('/api/posts')
const posts = await response.json()
set({ posts, isLoading: false })
} catch (err) {
set({ error: (err as Error).message, isLoading: false })
}
}
}))
在组件外访问 store
Zustand store 本身是一个独立的对象,不依赖 React 上下文,可以在任何地方使用:
// 读取当前 state(非响应式,适合一次性读取)
const currentCount = useCounterStore.getState().count
// 更新 state
useCounterStore.setState({ count: 10 })
useCounterStore.setState((state) => ({ count: state.count + 1 }))
// 订阅变化(适合在非 React 环境中使用,如工具函数、class 等)
const unsubscribe = useCounterStore.subscribe((state) => {
console.log('count changed:', state.count)
})
订阅 store 变化
// 基础订阅(任何字段变化都触发)
const unsubscribe = useStore.subscribe((newState, prevState) => {
if (newState.count !== prevState.count) {
sendAnalytics('count_changed', { value: newState.count })
}
})
// 在 React 组件外使用,需要在适当时机取消订阅
// 例如在模块销毁时:
window.addEventListener('beforeunload', unsubscribe)
与其他方案对比
vs Redux Toolkit
| 对比项 | Zustand | Redux Toolkit |
|---|---|---|
| 样板代码 | 极少 | 中等(slice/action/selector 分层) |
| 学习曲线 | 低 | 中等 |
| DevTools 支持 | 通过中间件支持 | 原生支持 |
| 适合场景 | 中小型应用、快速开发 | 大型团队、复杂业务逻辑 |
| 时间旅行调试 | 有限支持 | 完整支持 |
| 代码组织约束 | 无约束(灵活) | 有约束(规范) |
vs Jotai
| 对比项 | Zustand | Jotai |
|---|---|---|
| 思维模型 | 集中式 store(类似 Redux) | 原子化(类似 Recoil) |
| state 组织 | 按模块聚合 | 按原子分散 |
| 适合场景 | 有明确模块边界的业务状态 | 大量细粒度、相互依赖的状态 |
| 代码量 | 需要定义 store 结构 | 原子定义简洁 |
vs TanStack Query 的分工
两者不是竞争关系,而是互补:
| 工具 | 管理的状态类型 | 典型场景 |
|---|---|---|
| TanStack Query | 服务端状态(异步数据) | API 数据获取、缓存、同步 |
| Zustand | 客户端状态(本地 UI 状态) | 用户偏好、UI 交互状态、跨组件共享数据 |
推荐:服务端数据用 TanStack Query,本地 UI 状态用 Zustand,二者搭配使用。
最佳实践
使用细粒度选择器
// 不推荐:订阅整个 store,任何字段变化都触发重渲染
const store = useUserStore()
// 推荐:只订阅需要的字段
const username = useUserStore((state) => state.user?.name)
const isLoading = useUserStore((state) => state.isLoading)
选择器返回对象时使用 shallow 比较
import { shallow } from 'zustand/shallow'
// 返回对象时,默认用 Object.is 比较会导致每次都重渲染
// 使用 shallow 进行浅比较
const { count, name } = useStore(
(state) => ({ count: state.count, name: state.name }),
shallow
)
踩坑与注意事项
直接解构 store 会失去响应性
// 错误:解构赋值后,count 是普通变量,不具有响应性
const { count } = useCounterStore()
// count 不会随 store 变化自动更新
// 正确:通过选择器订阅
const count = useCounterStore((state) => state.count)
persist 中间件对 Map/Set 的序列化限制
JSON.stringify 无法正确序列化 Map、Set、Date 等类型,persist 默认使用 JSON 序列化,这些类型会丢失:
// 问题复现:Map 经过 JSON 序列化后变为 {}
const map = new Map('key', 'value')
JSON.parse(JSON.stringify(map)) // 输出 {}
// 解决方案:自定义 storage,手动处理序列化
const useStore = create<Store>()(
persist(
(set) => ({
dataMap: new Map<string, string>()
}),
{
name: 'store-with-map',
storage: createJSONStorage(() => localStorage, {
replacer: (key, value) => {
if (value instanceof Map) {
return { __type: 'Map', entries: Array.from(value.entries()) }
}
return value
},
reviver: (key, value) => {
if (value?.__type === 'Map') {
return new Map(value.entries)
}
return value
}
})
}
)
)
服务端渲染(SSR)场景的 hydration 问题
在 Next.js 等 SSR 框架中,服务端和客户端的初始 state 可能不一致,导致 hydration 报错。使用 skipHydration 并手动调用 rehydrate:
const useStore = create<Store>()(
persist(
(set) => ({ ... }),
{
name: 'ssr-store',
skipHydration: true // 跳过自动 hydration
}
)
)
// 在客户端组件中手动触发 hydration
useEffect(() => {
useStore.persist.rehydrate()
}, [])
避免在 store 外部直接修改 state 对象
// 错误:直接修改 getState() 返回的对象不会触发响应
const state = useStore.getState()
state.count = 10 // 不会触发任何订阅
// 正确:通过 setState 触发响应
useStore.setState({ count: 10 })
常见陷阱
陷阱:在 set 函数外直接修改 state 对象
现象: state.items.push(item) 后组件不重新渲染,数据已变但 UI 未更新。
原因: Zustand 用浅比较检测 state 变化,直接 mutate 不改变引用,订阅者不会被通知。
解决: 始终通过 set 返回新对象(配合 Immer 插件可以写可变风格):
// 错误:直接 push 不触发更新
set(state => { state.items.push(item) })
// 正确:返回新数组
set(state => ({ items: [...state.items, item] }))
// 或用 Immer 中间件
import { immer } from 'zustand/middleware/immer'
const useStore = create(immer((set) => ({
items: [],
addItem: (item) => set(state => { state.items.push(item) }),
})));
陷阱:在 Store 中订阅整个 state 导致不必要重渲染
现象: const store = useStore() 后,任何 state 字段变化都导致组件重渲染,包括无关字段。
原因: 不带 selector 的 useStore() 订阅整个 state 对象,任何变化都会触发重渲染。
解决: 用 selector 精确订阅需要的字段:
// 差:订阅整个 state
const { count, user } = useStore();
// 好:各自订阅各自的字段
const count = useStore(state => state.count);
const user = useStore(state => state.user);
陷阱:persist 中间件存储旧 schema 导致 hydration 错误
现象: 升级 store 结构后,localStorage 中仍是旧 schema 数据,导致类型错误或意外行为。
原因: persist 从存储层恢复数据时不做 schema 校验,旧结构直接合并到新 state。
解决: 配置 version 和 migrate 函数处理 schema 升级:
persist(store, {
name: 'app-store',
version: 2,
migrate: (persistedState: any, version) => {
if (version === 1) return { ...persistedState, newField: 'default' };
return persistedState;
},
})