zustand-state-management by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill zustand-state-management最后更新:2026-01-21 最新版本:zustand@5.0.10(发布于 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) // 仅在 bears 变化时重新渲染
const increase = useBearStore((state) => state.increase)
基础 Store(JavaScript):
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
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 }))
✅ 为持久化中间件的存储键使用唯一名称
✅ 使用 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 个已记录的问题:
错误:"文本内容与服务器渲染的 HTML 不匹配" 或 "水合失败"
来源:
原因:持久化中间件在客户端从 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)
},
},
),
)
// 在组件中
function MyComponent() {
const hasHydrated = useStore((state) => state._hasHydrated)
if (!hasHydrated) {
return <div>加载中...</div>
}
// 现在可以安全地使用持久化状态进行渲染
return <ActualContent />
}
错误:类型推断失败,StateCreator 类型与中间件一起使用时损坏
原因:柯里化语法 create<T>()() 是中间件与 TypeScript 推断协同工作所必需的。
预防:
// ❌ 错误 - 单括号
const useStore = create<MyStore>((set) => ({
// ...
}))
// ✅ 正确 - 双括号
const useStore = create<MyStore>()((set) => ({
// ...
}))
规则:在 TypeScript 中始终使用 create<T>()(),即使没有中间件(面向未来)。
错误:"导入尝试错误:'createJSONStorage' 未从 'zustand/middleware' 导出"
来源:GitHub Discussion #2839
原因:错误的导入路径或 zustand 与构建工具之间的版本不匹配。
预防:
// ✅ v5 的正确导入
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
// 验证版本
// zustand@5.0.9 包含 createJSONStorage
// zustand@4.x 使用不同的 API
// 检查你的 package.json
// "zustand": "^5.0.9"
错误:组件无限重新渲染,浏览器卡死
Uncaught Error: 超出最大更新深度。当组件在 componentWillUpdate 或 componentDidUpdate 中重复调用 setState 时会发生这种情况。
来源:
原因:在选择器中创建新的对象引用会导致 Zustand 认为状态已更改。
v5 破坏性变更:与 v4 相比,Zustand v5 使此错误更加明确。在 v4 中,此行为"不理想"但可能未被注意到。在 v5 中,你会立即看到"超出最大更新深度"错误。
预防:
import { useShallow } from 'zustand/shallow'
// ❌ 错误 - 每次都会创建新对象
const { bears, fishes } = useStore((state) => ({
bears: state.bears,
fishes: state.fishes,
}))
// ✅ 正确选项 1 - 分别选择原始值
const bears = useStore((state) => state.bears)
const fishes = useStore((state) => state.fishes)
// ✅ 正确选项 2 - 使用 useShallow 钩子处理多个值
const { bears, fishes } = useStore(
useShallow((state) => ({ bears: state.bears, fishes: state.fishes }))
)
错误:StateCreator 类型推断失败,复杂的中间件类型损坏
来源:官方切片模式指南
原因:组合多个切片需要显式的类型注解以确保中间件兼容性。
预防:
import { create, StateCreator } from 'zustand'
// 定义切片类型
interface BearSlice {
bears: number
addBear: () => void
}
interface FishSlice {
fishes: number
addFish: () => void
}
// 使用正确的类型创建切片
const createBearSlice: StateCreator<
BearSlice & FishSlice, // 组合的 store 类型
[], // 中间件修改器(如果没有则为空)
[], // 链式中间件(如果没有则为空)
BearSlice // 此切片的类型
> = (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 })),
})
// 组合切片
const useStore = create<BearSlice & FishSlice>()((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))
错误:并发重新水合尝试期间状态不一致
来源:
原因:在 Zustand v5.0.9 及更早版本中,持久化中间件初始化期间并发调用重新水合可能会导致竞态条件,其中多个水合尝试会相互干扰,导致状态不一致。
预防:升级到 Zustand v5.0.10 或更高版本。无需更改代码 —— 修复是持久化中间件内部的。
npm install zustand@latest # 确保 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 }), // 仅持久化 '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'(如上所示)。如果你看到"找不到模块:无法解析 'zustand/middleware/devtools'",请更新你的导入路径。
组合中间件(顺序很重要):
const useStore = create<MyStore>()(devtools(persist((set) => ({ /* ... */ }), { name: 'storage' }), { name: 'MyStore' }))
计算/派生值(在选择器中,不存储):
const count = useStore((state) => state.items.length) // 读取时计算
异步操作:
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(持久化/开发工具/immer/自定义), typescript-patterns.md(类型推断问题), nextjs-hydration.md(SSR/水合), migration-guide.md(从 Redux/Context/v4 迁移)
脚本:check-versions.sh(版本兼容性)
Vanilla 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 模式(见问题 #1)提供了另一种方法。
import { unstable_ssrSafe } from 'zustand/middleware'
const useStore = create<Store>()(
unstable_ssrSafe(
persist(
(set) => ({ /* state */ }),
{ name: 'my-store' }
)
)
)
建议:在此 API 稳定之前,继续使用问题 #1 中记录的 _hasHydrated 模式。关注 Discussion #2740 以获取此功能何时变为稳定的更新。
/pmndrs/zustand每周安装次数
1.2K
仓库
GitHub 星标数
656
首次出现
2026年1月20日
安全审计
安装于
opencode833
claude-code811
gemini-cli808
codex749
github-copilot676
cursor642
Last Updated : 2026-01-21 Latest Version : zustand@5.0.10 (released 2026-01-12) Dependencies : React 18-19, TypeScript 5+
npm install zustand
TypeScript Store (CRITICAL: use create<T>()() double parentheses):
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 })),
}))
Use in Components :
const bears = useBearStore((state) => state.bears) // Only re-renders when bears changes
const increase = useBearStore((state) => state.increase)
Basic Store (JavaScript):
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
TypeScript Store (Recommended):
interface CounterStore { count: number; increment: () => void }
const useStore = create<CounterStore>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
Persistent Store (survives page reloads):
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create<UserPreferences>()(
persist(
(set) => ({ theme: 'system', setTheme: (theme) => set({ theme }) }),
{ name: 'user-preferences', storage: createJSONStorage(() => localStorage) },
),
)
✅ Use create<T>()() (double parentheses) in TypeScript for middleware compatibility ✅ Define separate interfaces for state and actions ✅ Use selector functions to extract specific state slices ✅ Use set with updater functions for derived state: set((state) => ({ count: state.count + 1 })) ✅ Use unique names for persist middleware storage keys ✅ Handle Next.js hydration with hasHydrated flag pattern ✅ Use useShallow hook for selecting multiple values ✅ Keep actions pure (no side effects except state updates)
❌ Use create<T>(...) (single parentheses) in TypeScript - breaks middleware types ❌ Mutate state directly: set((state) => { state.count++; return state }) - use immutable updates ❌ Create new objects in selectors: useStore((state) => ({ a: state.a })) - causes infinite renders ❌ Use same storage name for multiple stores - causes data collisions ❌ Access localStorage during SSR without hydration check ❌ Use Zustand for server state - use TanStack Query instead ❌ Export store instance directly - always export the hook
This skill prevents 6 documented issues:
Error : "Text content does not match server-rendered HTML" or "Hydration failed"
Source :
Why It Happens : Persist middleware reads from localStorage on client but not on server, causing state mismatch.
Prevention :
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 />
}
Error : Type inference fails, StateCreator types break with middleware
Source : Official Zustand TypeScript Guide
Why It Happens : The currying syntax create<T>()() is required for middleware to work with TypeScript inference.
Prevention :
// ❌ WRONG - Single parentheses
const useStore = create<MyStore>((set) => ({
// ...
}))
// ✅ CORRECT - Double parentheses
const useStore = create<MyStore>()((set) => ({
// ...
}))
Rule : Always use create<T>()() in TypeScript, even without middleware (future-proof).
Error : "Attempted import error: 'createJSONStorage' is not exported from 'zustand/middleware'"
Source : GitHub Discussion #2839
Why It Happens : Wrong import path or version mismatch between zustand and build tools.
Prevention :
// ✅ CORRECT imports for v5
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
// Verify versions
// zustand@5.0.9 includes createJSONStorage
// zustand@4.x uses different API
// Check your package.json
// "zustand": "^5.0.9"
Error : Component re-renders infinitely, browser freezes
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
Source :
Why It Happens : Creating new object references in selectors causes Zustand to think state changed.
v5 Breaking Change : Zustand v5 made this error MORE explicit compared to v4. In v4, this behavior was "non-ideal" but could go unnoticed. In v5, you'll immediately see the "Maximum update depth exceeded" error.
Prevention :
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 }))
)
Error : StateCreator types fail to infer, complex middleware types break
Source : Official Slices Pattern Guide
Why It Happens : Combining multiple slices requires explicit type annotations for middleware compatibility.
Prevention :
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),
}))
Error : Inconsistent state during concurrent rehydration attempts
Source :
Why It Happens : In Zustand v5.0.9 and earlier, concurrent calls to rehydrate during persist middleware initialization could cause a race condition where multiple hydration attempts would interfere with each other, leading to inconsistent state.
Prevention : Upgrade to Zustand v5.0.10 or later. No code changes needed - the fix is internal to the persist middleware.
npm install zustand@latest # Ensure v5.0.10+
Note : This was fixed in v5.0.10 (January 2026). If you're using v5.0.9 or earlier and experiencing state inconsistencies with persist middleware, upgrade immediately.
Persist (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'
},
),
)
Devtools (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 Migration Note : In Zustand v4, devtools was imported from 'zustand/middleware/devtools'. In v5, use 'zustand/middleware' (as shown above). If you see "Module not found: Can't resolve 'zustand/middleware/devtools'", update your import path.
Combining Middlewares (order matters):
const useStore = create<MyStore>()(devtools(persist((set) => ({ /* ... */ }), { name: 'storage' }), { name: 'MyStore' }))
Computed/Derived Values (in selector, not stored):
const count = useStore((state) => state.items.length) // Computed on read
Async Actions :
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 })
},
}))
Resetting Store :
const initialState = { count: 0, name: '' }
const useStore = create<ResettableStore>()((set) => ({
...initialState,
reset: () => set(initialState),
}))
Selector with Params :
const todo = useStore((state) => state.todos.find((t) => t.id === id))
Templates : 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
References : middleware-guide.md (persist/devtools/immer/custom), typescript-patterns.md (type inference issues), nextjs-hydration.md (SSR/hydration), migration-guide.md (from Redux/Context/v4)
Scripts : check-versions.sh (version compatibility)
Vanilla Store (Without 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()
Custom Middleware :
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 Middleware (Mutable Updates):
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 Migration Note : If upgrading from v5.0.3 to v5.0.4+ and immer middleware stops working, verify you're using the import path shown above (zustand/middleware/immer). Some users reported issues after the v5.0.4 update that were resolved by confirming the correct import.
Experimental SSR Safe Middleware (v5.0.9+):
Status : Experimental (API may change)
Zustand v5.0.9 introduced experimental unstable_ssrSafe middleware for Next.js usage. This provides an alternative approach to the _hasHydrated pattern (see Issue #1).
import { unstable_ssrSafe } from 'zustand/middleware'
const useStore = create<Store>()(
unstable_ssrSafe(
persist(
(set) => ({ /* state */ }),
{ name: 'my-store' }
)
)
)
Recommendation : Continue using the _hasHydrated pattern documented in Issue #1 until this API stabilizes. Monitor Discussion #2740 for updates on when this becomes stable.
/pmndrs/zustandWeekly Installs
1.2K
Repository
GitHub Stars
656
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
opencode833
claude-code811
gemini-cli808
codex749
github-copilot676
cursor642
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装
AI Logo Creator - 使用 Gemini 和 Recraft 生成专业标志设计
1,000 周安装
Rust编码指南50条核心规则:命名、内存、并发、错误处理最佳实践
1,000 周安装
RAG 架构师指南:构建高效检索增强生成系统,优化向量存储与分块策略
1,000 周安装
marimo Python笔记本教程:交互式数据科学工具,替代Jupyter的响应式开发
1,000 周安装
RivetKit多人游戏开发模式:大逃杀、竞技场、IO游戏等10类模板与物理引擎指南
1,000 周安装
Flutter 缓存与性能优化指南:实现离线优先数据持久化与渲染加速
1,000 周安装