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 无法正确序列化 MapSetDate 等类型,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。
解决: 配置 versionmigrate 函数处理 schema 升级:

persist(store, {
  name: 'app-store',
  version: 2,
  migrate: (persistedState: any, version) => {
    if (version === 1) return { ...persistedState, newField: 'default' };
    return persistedState;
  },
})

参见

React完全指南
TanStack Query 完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog