React 生态的状态管理始终是前端架构的核心议题。从早期的 Context 传值方案,到 Redux 主导的单向数据流体系,再到如今百花齐放的轻量状态库,开发者始终在"架构规范"与"开发效率"之间寻找平衡。Zustand 作为其中极具代表性的方案,以极小的体积、极低的样板代码、基于 Hook 的原生使用体验,成为了中小型项目与复杂业务模块的首选状态管理工具。
官方将其定义为一套 small、fast、scalable 的状态管理方案:基于 Hook 设计,不强制特定架构,低样板代码,同时完整覆盖精细化订阅、中间件扩展、SSR 适配等工程能力。
一、状态复杂度:前端应用的"焦油坑"
经典软件工程论文《Out of the Tar Pit》中指出,可变状态是大型软件系统复杂性的首要来源。状态的分散、隐式共享与不可追溯 的修改,会让系统随着规模增长逐渐陷入"改一处崩一片"的维护困境。
前端应用同样遵循这一规律。当业务从单组件交互演进为跨模块联动时,局部的 useState 会迅速演变为层层透传的 props drilling,分散的状态会导致数据流追溯困难,渲染粒度失控也会带来显著的性能问题。
Zustand 的核心设计目标,正是解决这一问题:将跨组件共享的状态从 React 组件树中抽离,集中到独立的外部仓库中,通过精细化的订阅机制,实现"状态按需读取、变更精准触发"。
它既保留了单向数据流的可追溯性,又摒弃了冗余的架构约束,让开发者可以用最少的代码完成状态管理。
二、核心模型:外部 Store + 订阅机制
2.1 Store 的基本构成
Zustand 的 store 本质上是一个包含状态(state)、动作(actions)与订阅能力的独立对象,可以抽象为:
store = state + actions + subscribe API
官方提供 create 函数创建绑定 React 的 store Hook,其返回值既是自定义 Hook,也挂载了完整的命令式 API:getState、setState、getInitialState、subscribe,兼顾声明式视图绑定与命令式业务调用。
基础示例:
tsx
import { create } from 'zustand'
type CounterStore = {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export 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 }),
}))
组件中通过 selector 订阅状态:
tsx
function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
return <button onClick={increment}>count: {count}</button>
}
2.2 与全局变量的本质区别
普通全局变量的核心问题在于变更不可感知:变量修改后,React 组件无法获知变化,也就不会触发重渲染。
Zustand 则内置了完整的发布-订阅机制:状态变更后,会主动通知所有订阅了对应状态片段的组件触发更新 。
其底层在 React 侧基于官方 useSyncExternalStore Hook 实现------这是 React 专门为第三方外部状态库提供的标准接入接口,保证了并发渲染下的安全性与水合一致性。
因此 Zustand 的准确定位是:可订阅的外部状态容器,而非简单的全局变量。
2.3 状态更新机制:浅合并与不可变性
Zustand 通过 set 函数执行状态更新,其默认行为是单层浅合并(shallow merge):传入的新状态会与当前根级状态进行合并,未显式声明的字段会被保留。
ts
// 仅更新 name 字段,age 字段自动保留
set({ name: 'New Name' })
这一设计极大减少了手动展开根状态的样板代码,但需要注意:浅合并仅作用于根层级。对于嵌套对象,开发者需要手动执行不可变更新,逐层展开对象结构:
ts
set((state) => ({
user: {
...state.user,
profile: {
...state.user.profile,
city: 'Beijing',
},
},
}))
如果需要完全替换整个 store 状态,可以传入第二个参数 replace: true:
ts
set(newState, true) // 完全替换状态,不执行合并
注意:与 React 状态一致,Zustand 要求所有状态更新遵循不可变原则。直接修改原状态对象不会触发订阅通知,也不会导致组件重渲染。
三、渲染优化:Selector 与精细化更新
3.1 Selector 的价值
Selector 是 Zustand 性能优化的核心机制。组件通过传入 selector 函数,声明自己只关心 store 中的某一部分数据。
tsx
// 只订阅 theme 字段
const theme = useAppStore((state) => state.theme)
当 store 发生变更时,Zustand 会对 selector 的返回值进行 Object.is 严格相等比较:只有当返回值发生变化时,才会触发当前组件重渲染。
这意味着:
- 只修改
count字段时,订阅theme的组件不会重渲染 - 状态粒度拆分越合理,不必要的重渲染就越少
这也是 Zustand 相比原生 Context 的核心优势:Context 无法做到字段级别的更新分发,Provider value 变化后所有消费组件都会全量重渲染。
3.2 useShallow:解决复合 selector 的引用问题
当 selector 需要返回对象、数组等复合类型时,会出现一个经典陷阱:每次 selector 执行都会创建新的引用,即使内部数据完全一致,Object.is 也会判定为变化,从而引发不必要的重渲染。
tsx
// ❌ 不推荐:每次渲染都生成新对象,可能导致多余重渲染
const { user, theme } = useAppStore((state) => ({
user: state.user,
theme: state.theme,
}))
Zustand 官方提供 useShallow 工具来解决这一问题,它会对 selector 返回值进行浅层相等比较,而非引用比较:
tsx
import { useShallow } from 'zustand/react/shallow'
// ✅ 推荐:浅层比较,字段引用不变则不触发重渲染
const { user, theme } = useAppStore(
useShallow((state) => ({
user: state.user,
theme: state.theme,
}))
)
除对象外,useShallow 同样适用于数组、Object.keys() 等计算结果,是日常开发中最常用的优化工具。
四、中间件体系:按需扩展能力
Zustand 遵循"核心极小,能力可扩展"的设计理念,通过中间件(middleware)机制提供进阶能力。以下是四类最常用的官方中间件。
4.1 persist:状态持久化
persist 中间件可以将 store 状态持久化到本地存储中,页面刷新后自动恢复,适合保存用户偏好、配置类数据。默认存储介质为 localStorage,也支持自定义 sessionStorage、IndexedDB 等存储方案。
tsx
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
type ThemeStore = {
theme: 'light' | 'dark'
setTheme: (theme: 'light' | 'dark') => void
}
export const useThemeStore = create<ThemeStore>()(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{
name: 'theme-storage', // 存储键名,必须唯一
storage: createJSONStorage(() => sessionStorage), // 可选,默认 localStorage
}
)
)
注意:仅持久化非敏感、低频率变更的配置数据;Token、接口缓存、大数据量状态尽量不要放入持久化存储。
4.2 devtools:调试能力接入
devtools 中间件让 Zustand 可以无缝对接 Redux DevTools 扩展,实现状态回溯、action 追踪、时间旅行调试等能力,在复杂业务中极具价值。
tsx
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const useTaskStore = create<TaskStore>()(
devtools(
(set) => ({
// ...状态与动作
}),
{ name: 'TaskStore' } // 在 DevTools 中区分多个 store
)
)
开发中可以为每个 set 调用指定 action 名称,提升调试可读性:
ts
set({ taskList: newList }, undefined, 'task/updateList')
同时支持通过 actionsDenylist 过滤敏感或高频内部 action,避免调试面板被无用信息淹没。
4.3 immer:简化嵌套不可变更新
对于深层嵌套的状态结构,手动逐层展开对象既繁琐又易出错。immer 中间件允许以"可变写法"执行不可变更新,底层由 Immer 库自动生成不可变副本。
tsx
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useUserStore = create<UserStore>()(
immer((set) => ({
updateAge: (age) =>
set((state) => {
state.user.profile.age = age // 直接"修改",底层自动生成新对象
}),
}))
)
4.4 subscribeWithSelector:精细化订阅
原生 subscribe 只能监听整个 store 的变化,而 subscribeWithSelector 中间件扩展了订阅能力,支持在组件外或非 React 环境中订阅指定状态片段,并获取变更前后的值。
tsx
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
const useDeviceStore = create<DeviceStore>()(
subscribeWithSelector((set) => ({
connected: false,
// ...
}))
)
// 订阅单个字段变化,可获取上一状态值
const unsubscribe = useDeviceStore.subscribe(
(state) => state.connected,
(connected, prevConnected) => {
console.log('连接状态变更:', prevConnected, '->', connected)
}
)
该能力非常适合 WebSocket 监听、日志埋点、非 React 业务模块与状态层的联动场景。
五、模块化架构:Slices 模式
随着业务迭代,单个 store 会不断膨胀,最终变成难以维护的"上帝对象"。Zustand 官方提供 Slices Pattern(切片模式) 来解决大型 store 的模块化拆分问题。
5.1 基本用法
将不同业务领域的状态与动作拆分为独立的 slice 创建函数,每个函数接收 set、get、store 作为参数,返回对应领域的状态片段。
ts
// slices/bearSlice.ts
export const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
})
// slices/fishSlice.ts
export const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
在根 store 中组合所有切片:
ts
// stores/useBoundStore.ts
import { create } from 'zustand'
import { createBearSlice } from './slices/bearSlice'
import { createFishSlice } from './slices/fishSlice'
export const useBoundStore = create((...args) => ({
...createBearSlice(...args),
...createFishSlice(...args),
}))
5.2 跨切片交互
切片之间可以通过 get() 访问完整 store,调用其他切片的 action,实现跨领域的组合动作:
ts
export const createBearFishSlice = (set, get) => ({
addBearAndFish: () => {
get().addBear()
get().addFish()
},
})
5.3 注意事项
- 中间件仅作用于根 store :不要在单个 slice 内部应用中间件,所有中间件都应包裹在最外层的
create调用中,避免出现不可预期的行为。 - TypeScript 支持:切片模式需要显式声明联合类型,保证跨切片的类型推导正确。
- 拆分粒度:按业务领域拆分,而非按技术属性拆分,避免过度碎片化。
六、框架无关:Vanilla Store 与分层设计
Zustand 的状态核心并不与 React 绑定,官方提供 createStore 用于创建纯 JavaScript 的 vanilla store,可运行于任何 JS 环境。
ts
import { createStore } from 'zustand/vanilla'
const robotStore = createStore((set) => ({
connected: false,
setConnected: (connected) => set({ connected }),
}))
Vanilla store 提供完整的命令式 API:
robotStore.getState():读取当前状态robotStore.setState(newState):更新状态robotStore.subscribe(callback):订阅状态变化
在 React 组件中使用时,通过 useStore Hook 进行绑定:
tsx
import { useStore } from 'zustand'
function StatusBadge() {
const connected = useStore(robotStore, (state) => state.connected)
return <span>{connected ? '在线' : '离线'}</span>
}
这一设计体现了清晰的分层思想:状态核心与 UI 框架解耦,业务逻辑层可以完全基于 vanilla store 编写,再由 React 层做视图绑定,非常适合跨端、多渲染环境的复杂项目。
七、Next.js 全栈场景
在 Next.js 这类服务端渲染框架中,Zustand 的使用需要特别注意生命周期与请求隔离问题,否则会出现跨请求状态污染、水合错误等严重问题。
7.1 问题:全局单例的风险
Node.js 服务端是长驻进程,如果将 store 定义为模块级全局变量,多个用户请求会共享同一个 store 实例,导致不同用户的数据互相泄漏。
因此 SSR 场景下的核心原则是:按请求创建 store 实例,禁止跨请求共享状态。
7.2 标准实现方案:工厂函数 + Context
官方推荐的标准方案分为三步:
第一步:编写 store 工厂函数
不直接创建 store 实例,而是导出一个创建函数,每次调用生成全新的 store。
ts
// src/stores/counter-store.ts
import { createStore } from 'zustand/vanilla'
export type CounterState = { count: number }
export type CounterActions = {
increment: () => void
decrement: () => void
}
export type CounterStore = CounterState & CounterActions
export const defaultInitState: CounterState = { count: 0 }
export const createCounterStore = (
initState: CounterState = defaultInitState
) => {
return createStore<CounterStore>()((set) => ({
...initState,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
}
第二步:封装 Context Provider
通过 React Context 将 per-request 的 store 实例向下传递,同时用 useState 保证实例在组件生命周期内唯一,避免重渲染导致 store 重置。
tsx
// src/providers/counter-store-provider.tsx
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
import { useStore } from 'zustand'
import { createCounterStore, CounterStore } from '@/stores/counter-store'
export type CounterStoreApi = ReturnType<typeof createCounterStore>
const CounterStoreContext = createContext<CounterStoreApi | undefined>(undefined)
export function CounterStoreProvider({ children }: { children: ReactNode }) {
const [store] = useState(() => createCounterStore())
return (
<CounterStoreContext.Provider value={store}>
{children}
</CounterStoreContext.Provider>
)
}
export function useCounterStore<T>(selector: (store: CounterStore) => T): T {
const store = useContext(CounterStoreContext)
if (!store) throw new Error('useCounterStore must be used within CounterStoreProvider')
return useStore(store, selector)
}
第三步:在布局/页面中注入 Provider
- App Router 中,在
layout.tsx中全局注入,或在单页面中按路由注入; - Pages Router 中,在
_app.tsx全局注入,或在页面组件中注入。
7.3 关键注意事项
- RSC 禁止读写 store:React Server Components 无法使用 Hook 与 Context,也不应持有状态,所有 Zustand 相关逻辑都必须放在 Client Component 中。
- 水合一致性:服务端初始化的状态需要完整传递到客户端,保证首次渲染输出一致,避免水合不匹配错误。
- 路由级 store 隔离:如果不同路由需要独立的 store 实例,可以将 Provider 下沉到页面组件级别。
八、工程边界与常见误区
8.1 状态归属的判断原则
Zustand 不是"万能状态桶",合理的架构需要明确状态边界。
| 状态类型 | 推荐方案 | 不建议放入 Zustand 的原因 |
|---|---|---|
| 组件局部状态(输入框、弹窗) | useState / useReducer |
作用域仅限组件,全局存储无意义 |
| 表单状态与校验 | React Hook Form | 表单有独立的交互、校验、脏值管理体系 |
| 服务端数据与缓存 | TanStack Query / SWR | 缓存、失效、重试、去重等是专业数据请求库的核心能力 |
| URL 路由参数 | React Router / URLSearchParams | URL 是唯一真相源,不应与本地状态重复同步 |
| 复杂状态机逻辑 | XState | 有限状态机有专门的建模与调试工具 |
| 超大规模团队强规范业务 | Redux Toolkit | 强约束架构更适合百人级团队协作 |
8.2 四类典型错误写法
错误1:全量订阅 store
tsx
const store = useAppStore() // ❌ 任何字段变化都会触发重渲染
始终按需选择最小粒度的状态字段,只订阅组件真正需要的数据。
错误2:复合 selector 不使用浅层比较
返回对象/数组却不加 useShallow,是最常见的性能问题来源。
错误3:巨型单 store
将所有业务状态塞进一个 useGlobalStore 会导致文件膨胀、职责不清、类型推导变慢。应按业务领域拆分多个独立 store,或采用切片模式。
错误4:高频数据全部灌入 Zustand
对于 100Hz 以上的实时数据(如机器人位姿、视频帧、点云),不要每帧更新 store 触发 React 重渲染。正确的分层方式是:
- 高频原始数据:存放于外部对象、环形缓冲区或 Web Worker 中
- UI 展示数据:降采样后同步到 Zustand(如 10Hz)
- 可视化绘制:直接由 Canvas/WebGL 驱动,不经过 React 渲染链路
九、横向对比:与主流方案的差异
9.1 vs Redux / Redux Toolkit
Redux 强调强架构约束,遵循严格的单向数据流:dispatch(action) -> reducer -> new state,配套完整的开发工具与生态规范,适合大型团队协作。
Zustand 则弱化了架构约束,没有强制的 action type 与 reducer,直接通过函数修改状态,样板代码极少,上手成本低。
二者并非替代关系:团队规模小、业务迭代快的项目适合 Zustand;超大规模、多团队协作的项目更适合 Redux Toolkit。
9.2 vs React Context
Context 本质是跨层级依赖注入机制,而非专业状态管理方案。它没有精细化订阅能力,value 变化会导致所有消费组件全量更新,适合低频、全局配置类数据(主题、语言、权限配置)。
Zustand 是专业的外部状态管理库,具备 selector 精细化更新、中间件、命令式 API 等完整能力,适合高频更新的业务状态。
9.3 与 Flux 的关系
Zustand 官方自述遵循"简化版 Flux 原则"。它保留了 Flux"状态集中管理、单向更新、视图响应状态"的核心思想,但去掉了 Dispatcher、action type、reducer 等强约束,将流程简化为 action -> set -> store -> subscribed view,在保持可追溯性的同时大幅降低了使用门槛。
总结
Zustand 的成功,本质上是"极简主义"在状态管理领域的胜利。它没有发明全新的范式,而是把外部状态、订阅机制、selector 优化这些早已被验证的理念,以极低的成本封装成了开发者友好的 API。
它的核心设计哲学可以概括为三点:
- 核心极简:只做状态容器与订阅分发,不强制架构,不侵入业务代码
- 按需扩展:通过中间件机制叠加持久化、调试、不可变更新等能力
- 框架无关:状态核心与 React 解耦,适配全场景运行环境
对于大多数中小型 React 项目,以及工业前端、设备控制台、编辑器这类业务复杂但团队规模有限的场景,Zustand 是兼顾开发效率与可维护性的最优解之一。