mcpField
Z
zustand-state-management

zustand-state-management

github.com/jezweb开发1.1k1.2k 次安装3 周前更新
代码仓库

关于 zustand-state-management

面向 React 和 TypeScript 开发者的 Zustand 状态管理指南,涵盖 store 创建、选择器、中间件及常见问题处理。还提供持久化、Next.js 水合、异步操作等模式与模板。

主要功能

  1. 01JavaScript 与 TypeScript store 模式
  2. 02选择器与状态更新建议
  3. 03持久化和 Devtools 中间件
  4. 04Next.js 水合问题处理
  5. 05异步操作与派生值模式
  6. 06迁移指南及代码模板

适用场景

  1. 01为 React 应用设计状态 store
  2. 02设置持久化状态与 Devtools
  3. 03处理 Next.js 水合不匹配
  4. 04排查选择器导致的重复渲染

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill zustand-state-management

安装后在 Claude Code 中输入/zustand-state-management确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
example-reference.md
middleware-guide.md
migration-guide.md
nextjs-hydration.md
typescript-patterns.md
rules/
zustand-state-management.md
scripts/
check-versions.sh
example-script.sh
templates/
async-actions-store.ts
basic-store.ts
computed-store.ts
devtools-store.ts
nextjs-store.ts
persist-store.ts
slices-pattern.ts
typescript-store.ts

详细介绍

Zustand 状态管理

最后更新:2026-01-21 最新版本:[email protected](发布于 2026-01-12) 依赖项:React 18-19、TypeScript 5+


快速开始

npm install zustand

TypeScript Store(重要:使用 create<T>()() 双括号):

import { create } from 'zustand'

interface BearStore {
  bears: number
  increase: (by: number) => void
}

const useBearStore = create<BearStore>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}))

在组件中使用:

const bears = useBearStore((state) => state.bears)  // Only re-renders when bears changes
const increase = useBearStore((state) => state.increase)

核心模式

基本 Store(JavaScript):

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

TypeScript Store(推荐):

interface CounterStore { count: number; increment: () => void }
const useStore = create<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

持久化 Store(页面重新加载后仍然保留):

import { persist, createJSONStorage } from 'zustand/middleware'

const useStore = create<UserPreferences>()(
  persist(
    (set) => ({ theme: 'system', setTheme: (theme) => set({ theme }) }),
    { name: 'user-preferences', storage: createJSONStorage(() => localStorage) },
  ),
)

重要规则

始终要做

✅ 在 TypeScript 中使用 create<T>()()(双括号)以兼容中间件 ✅ 为状态和操作分别定义接口 ✅ 使用选择器函数提取特定的状态片段 ✅ 对派生状态使用带更新函数的 set:set((state) => ({ count: state.count + 1 })) ✅ 为 persist 中间件的存储键使用唯一名称 ✅ 使用 hasHydrated 标志模式处理 Next.js 水合 ✅ 使用 useShallow 钩子选择多个值 ✅ 保持操作纯净(除状态更新外不产生副作用)

绝不要做

❌ 在 TypeScript 中使用 create<T>(...)(单括号)——会破坏中间件类型 ❌ 直接修改状态:set((state) => { state.count++; return state })——应使用不可变更新 ❌ 在选择器中创建新对象:useStore((state) => ({ a: state.a }))——会导致无限次渲染 ❌ 多个 Store 使用相同的存储名称——会造成数据冲突 ❌ 未检查水合状态就在 SSR 期间访问 localStorage ❌ 使用 Zustand 管理服务端状态——应改用 TanStack Query ❌ 直接导出 Store 实例——始终导出钩子


已知问题预防

此技能可预防 6 个已记录的问题:

问题 #1:Next.js 水合不匹配

错误:"Text content does not match server-rendered HTML" 或 "Hydration failed"

来源:

原因: Persist 中间件会在客户端从 localStorage 读取数据,但不会在服务端读取,导致状态不匹配。

预防方法:

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface StoreWithHydration {
  count: number
  _hasHydrated: boolean
  setHasHydrated: (hydrated: boolean) => void
  increase: () => void
}

const useStore = create<StoreWithHydration>()(
  persist(
    (set) => ({
      count: 0,
      _hasHydrated: false,
      setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }),
      increase: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'my-store',
      onRehydrateStorage: () => (state) => {
        state?.setHasHydrated(true)
      },
    },
  ),
)

// In component
function MyComponent() {
  const hasHydrated = useStore((state) => state._hasHydrated)

  if (!hasHydrated) {
    return <div>Loading...</div>
  }

  // Now safe to render with persisted state
  return <ActualContent />
}
问题 #2:遗漏 TypeScript 双括号

错误:类型推断失败,StateCreator 类型与中间件不兼容

来源:Zustand 官方 TypeScript 指南

原因: 中间件要与 TypeScript 类型推断配合工作,必须使用柯里化语法 create<T>()()。

预防方法:

// ❌ WRONG - Single parentheses
const useStore = create<MyStore>((set) => ({
  // ...
}))

// ✅ CORRECT - Double parentheses
const useStore = create<MyStore>()((set) => ({
  // ...
}))

规则:在 TypeScript 中始终使用 create<T>()(),即使没有中间件也一样(以确保未来兼容)。

问题 #3:Persist 中间件导入错误

错误:"Attempted import error: 'createJSONStorage' is not exported from 'zustand/middleware'"

来源:GitHub 讨论 #2839

原因: 导入路径错误,或 zustand 与构建工具之间的版本不匹配。

预防方法:

// ✅ CORRECT imports for v5
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

// Verify versions
// [email protected] includes createJSONStorage
// [email protected] uses different API

// Check your package.json
// "zustand": "^5.0.9"
问题 #4:无限渲染循环

错误:组件不断重新渲染,浏览器冻结

Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.

来源:

原因: 在选择器中创建新的对象引用,会让 Zustand 认为状态已更改。

v5 重大变更:与 v4 相比,Zustand v5 会更明确地暴露此错误。在 v4 中,这种行为虽然“并不理想”,但可能不易被察觉。在 v5 中,你会立即看到 "Maximum update depth exceeded" 错误。

预防方法:

import { useShallow } from 'zustand/shallow'

// ❌ WRONG - Creates new object every time
const { bears, fishes } = useStore((state) => ({
  bears: state.bears,
  fishes: state.fishes,
}))

// ✅ CORRECT Option 1 - Select primitives separately
const bears = useStore((state) => state.bears)
const fishes = useStore((state) => state.fishes)

// ✅ CORRECT Option 2 - Use useShallow hook for multiple values
const { bears, fishes } = useStore(
  useShallow((state) => ({ bears: state.bears, fishes: state.fishes }))
)
问题 #5:Slices 模式的 TypeScript 复杂性

错误:StateCreator 类型推断失败,复杂的中间件类型无法正常工作

来源:官方 Slices 模式指南

原因: 组合多个切片时,需要显式标注类型才能兼容中间件。

预防方法:

import { create, StateCreator } from 'zustand'

// Define slice types
interface BearSlice {
  bears: number
  addBear: () => void
}

interface FishSlice {
  fishes: number
  addFish: () => void
}

// Create slices with proper types
const createBearSlice: StateCreator<
  BearSlice & FishSlice,  // Combined store type
  [],                      // Middleware mutators (empty if none)
  [],                      // Chained middleware (empty if none)
  BearSlice               // This slice's type
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
})

const createFishSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

// Combine slices
const useStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))
Issue #6:持久化中间件竞态条件(已在 v5.0.10+ 修复)

错误:并发重新水合尝试期间状态不一致

来源:

发生原因: 在 Zustand v5.0.9 及更早版本中,持久化中间件初始化期间并发调用重新水合,可能导致竞态条件:多个水合尝试相互干扰,造成状态不一致。

预防方法: 升级到 Zustand v5.0.10 或更高版本。无需更改代码——此修复已在持久化中间件内部实现。

npm install zustand@latest  # Ensure v5.0.10+

注意:此问题已在 v5.0.10 中修复(2026 年 1 月)。如果你使用的是 v5.0.9 或更早版本,并且持久化中间件导致状态不一致,请立即升级。


中间件

持久化(localStorage):

import { persist, createJSONStorage } from 'zustand/middleware'

const useStore = create<MyStore>()(
  persist(
    (set) => ({ data: [], addItem: (item) => set((state) => ({ data: [...state.data, item] })) }),
    {
      name: 'my-storage',
      partialize: (state) => ({ data: state.data }),  // Only persist 'data'
    },
  ),
)

开发者工具(Redux DevTools):

import { devtools } from 'zustand/middleware'

const useStore = create<CounterStore>()(
  devtools(
    (set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 }), undefined, 'increment') }),
    { name: 'CounterStore' },
  ),
)

v4→v5 迁移说明:在 Zustand v4 中,devtools 从 'zustand/middleware/devtools' 导入。在 v5 中,请使用 'zustand/middleware'(如上所示)。如果看到 “Module not found: Can't resolve 'zustand/middleware/devtools'”,请更新导入路径。

组合中间件(顺序很重要):

const useStore = create<MyStore>()(devtools(persist((set) => ({ /* ... */ }), { name: 'storage' }), { name: 'MyStore' }))

常见模式

计算值/派生值(在选择器中计算,而非存储):

const count = useStore((state) => state.items.length)  // Computed on read

异步操作:

const useAsyncStore = create<AsyncStore>()((set) => ({
  data: null,
  isLoading: false,
  fetchData: async () => {
    set({ isLoading: true })
    const response = await fetch('/api/data')
    set({ data: await response.text(), isLoading: false })
  },
}))

重置 Store:

const initialState = { count: 0, name: '' }
const useStore = create<ResettableStore>()((set) => ({
  ...initialState,
  reset: () => set(initialState),
}))

带参数的选择器:

const todo = useStore((state) => state.todos.find((t) => t.id === id))

随附资源

模板:basic-store.ts、typescript-store.ts、persist-store.ts、slices-pattern.ts、devtools-store.ts、nextjs-store.ts、computed-store.ts、async-actions-store.ts

参考资料:middleware-guide.md(persist/devtools/immer/custom)、typescript-patterns.md(类型推断问题)、nextjs-hydration.md(SSR/水合)、migration-guide.md(从 Redux/Context/v4 迁移)

脚本:check-versions.sh(版本兼容性)


高级主题

原生 Store(不使用 React):

import { createStore } from 'zustand/vanilla'

const store = createStore<CounterStore>()((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })) }))
const unsubscribe = store.subscribe((state) => console.log(state.count))
store.getState().increment()

自定义中间件:

const logger: Logger = (f, name) => (set, get, store) => {
  const loggedSet: typeof set = (...a) => { set(...a); console.log(`[${name}]:`, get()) }
  return f(loggedSet, get, store)
}

Immer 中间件(可变更新):

import { immer } from 'zustand/middleware/immer'

const useStore = create<TodoStore>()(immer((set) => ({
  todos: [],
  addTodo: (text) => set((state) => { state.todos.push({ id: Date.now().toString(), text }) }),
})))

v5.0.3→v5.0.4 迁移说明:如果从 v5.0.3 升级到 v5.0.4+ 后 immer 中间件停止工作,请确认使用的是上方所示的导入路径(zustand/middleware/immer)。一些用户报告称,确认导入路径正确后,v5.0.4 更新后遇到的问题得到了解决。

实验性 SSR 安全中间件(v5.0.9+):

状态:实验性(API 可能会更改)

Zustand v5.0.9 引入了实验性的 unstable_ssrSafe 中间件,供 Next.js 使用。这为 _hasHydrated 模式提供了另一种方案(参见 Issue #1)。

import { unstable_ssrSafe } from 'zustand/middleware'

const useStore = create<Store>()(
  unstable_ssrSafe(
    persist(
      (set) => ({ /* state */ }),
      { name: 'my-store' }
    )
  )
)

建议:在此 API 稳定之前,继续使用 Issue #1 中介绍的 _hasHydrated 模式。请关注 Discussion #2740,了解该 API 何时稳定的最新消息。


官方文档

评论

支持 Markdown