关于 zustand
Zustand 是面向 React 应用的轻量状态管理库,使用基于 Hook 的 API 创建和访问状态。该指南还介绍中间件、异步操作、选择器以及 Next.js 和 SSR 模式。
主要功能
- 01基于 Hook 的状态管理
- 02选择器与订阅
- 03持久化、DevTools 和 Immer 中间件
- 04异步操作与乐观更新
- 05切片模式与 TypeScript
- 06SSR 水合模式
适用场景
- 01在 React 应用中管理全局状态
- 02为状态添加持久化或 DevTools
- 03组织复杂状态逻辑和异步操作
- 04在 Next.js 应用中处理状态水合
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill zustand安装后在 Claude Code 中输入/zustand确认已加载。
附带文件metadata.json
详细介绍
Zustand 状态管理
概述
Zustand 是一个精简且不强加约定的 React 状态管理库。无需 Provider,也没有样板代码——只需一个简单的基于 Hook 的 API,在 React 应用中使用起来十分自然。
适用场景
- 需要全局状态,但不想承担 Redux 复杂性的 React 应用
- 希望减少样板代码和打包体积的项目
- 偏好直接修改状态而非使用 reducer 的团队
- 需要灵活状态水合的 SSR 应用(Next.js)
- 从 Redux/Context API 迁移到更简单方案的项目
快速开始
npm install zustand
// stores/useCounterStore.ts
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
decrement: () => void
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// components/Counter.tsx
import { useCounterStore } from '@/stores/useCounterStore'
export function Counter() {
const { count, increment, decrement } = useCounterStore()
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
Zustand 完整指南
核心概念
创建 Store
import { create } from 'zustand'
// Basic store
interface BearState {
bears: number
addBear: () => void
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
}))
// Store with get access
const useStore = create<State>((set, get) => ({
count: 0,
increment: () => {
const currentCount = get().count
set({ count: currentCount + 1 })
},
}))
状态访问模式
// Select entire store (re-renders on any change)
const state = useStore()
// Select specific fields (re-renders only when these change)
const bears = useStore((state) => state.bears)
const addBear = useStore((state) => state.addBear)
// Destructure with selector
const { bears, addBear } = useStore((state) => ({
bears: state.bears,
addBear: state.addBear,
}))
// Multiple selectors
const bears = useStore((state) => state.bears)
const fish = useStore((state) => state.fish)
修改状态
interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
removeTodo: (id: string) => void
}
const useTodoStore = create<TodoState>((set) => ({
todos: [],
// Add item
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: nanoid(), text, completed: false }]
})),
// Update item
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
// Remove item
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),
}))
React 集成
useStore Hook
function BearCounter() {
// Re-renders when bears changes
const bears = useBearStore((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
// Doesn't re-render when bears changes
const addBear = useBearStore((state) => state.addBear)
return <button onClick={addBear}>Add bear</button>
}
浅比较
import { shallow } from 'zustand/shallow'
// Prevent re-renders when object identity changes but values don't
const { nuts, honey } = useBearStore(
(state) => ({ nuts: state.nuts, honey: state.honey }),
shallow
)
// Custom equality function
const treats = useBearStore(
(state) => state.treats,
(prev, next) => prev.length === next.length
)
在 React 组件外部使用
// Read state
const count = useStore.getState().count
// Subscribe to changes
const unsubscribe = useStore.subscribe(
(state) => console.log('Count changed:', state.count)
)
// Update state
useStore.setState({ count: 42 })
// Update with function
useStore.setState((state) => ({ count: state.count + 1 }))
TypeScript 模式
创建带类型的 Store
interface UserState {
user: User | null
setUser: (user: User) => void
clearUser: () => void
}
const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}))
// Type inference works automatically
const user = useUserStore((state) => state.user) // User | null
Store 类型推断
// Extract store type
type UserStoreState = ReturnType<typeof useUserStore.getState>
// Selector type helper
type Selector<T> = (state: UserState) => T
const selectUsername: Selector<string | undefined> = (state) =>
state.user?.name
组合多个 Store
// Type-safe store combination
function useHybridStore<T, U>(
selector1: (state: State1) => T,
selector2: (state: State2) => U
): [T, U] {
return [
useStore1(selector1),
useStore2(selector2),
]
}
const [user, theme] = useHybridStore(
(s) => s.user,
(s) => s.theme
)
Slices 模式
创建 Slices
// authSlice.ts
export interface AuthSlice {
user: User | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
}
export const createAuthSlice: StateCreator<
AuthSlice & TodoSlice,
[],
[],
AuthSlice
> = (set) => ({
user: null,
login: async (credentials) => {
const user = await api.login(credentials)
set({ user })
},
logout: () => set({ user: null }),
})
// todoSlice.ts
export interface TodoSlice {
todos: Todo[]
addTodo: (text: string) => void
}
export const createTodoSlice: StateCreator<
AuthSlice & TodoSlice,
[],
[],
TodoSlice
> = (set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: nanoid(), text, completed: false }]
})),
})
// store.ts
import { create } from 'zustand'
import { createAuthSlice, AuthSlice } from './authSlice'
import { createTodoSlice, TodoSlice } from './todoSlice'
export const useStore = create<AuthSlice & TodoSlice>()((...a) => ({
...createAuthSlice(...a),
...createTodoSlice(...a),
}))
Slice 间通信
export const createTodoSlice: StateCreator<
AuthSlice & TodoSlice,
[],
[],
TodoSlice
> = (set, get) => ({
todos: [],
addTodo: (text) => {
// Access other slice's state
const user = get().user
if (!user) throw new Error('Not authenticated')
set((state) => ({
todos: [...state.todos, {
id: nanoid(),
text,
userId: user.id,
completed: false
}]
}))
},
})
中间件
Persist 中间件
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface PreferencesState {
theme: 'light' | 'dark'
language: string
setTheme: (theme: 'light' | 'dark') => void
}
export const usePreferencesStore = create<PreferencesState>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme }),
}),
{
name: 'preferences-storage', // localStorage key
storage: createJSONStorage(() => localStorage),
// Partial persistence
partialize: (state) => ({ theme: state.theme }),
// Migration between versions
version: 1,
migrate: (persistedState: any, version: number) => {
if (version === 0) {
// Migrate from v0 to v1
persistedState.language = 'en'
}
return persistedState as PreferencesState
},
}
)
)
// Custom storage (e.g., AsyncStorage for React Native)
const customStorage = {
getItem: async (name: string) => {
const value = await AsyncStorage.getItem(name)
return value ?? null
},
setItem: async (name: string, value: string) => {
await AsyncStorage.setItem(name, value)
},
removeItem: async (name: string) => {
await AsyncStorage.removeItem(name)
},
}
const useStore = create(
persist(
(set) => ({ /* ... */ }),
{
name: 'app-storage',
storage: createJSONStorage(() => customStorage)
}
)
)
DevTools 中间件
import { devtools } from 'zustand/middleware'
interface CounterState {
count: number
increment: () => void
}
const useCounterStore = create<CounterState>()(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
}),
{
name: 'CounterStore',
enabled: process.env.NODE_ENV === 'development'
}
)
)
// Action names in Redux DevTools
set({ count: 42 }, false, 'setCount')
set((state) => ({ count: state.count + 1 }), false, { type: 'increment', amount: 1 })
Immer 中间件
import { immer } from 'zustand/middleware/immer'
interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
}
const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
// Mutate state directly with Immer
addTodo: (text) => set((state) => {
state.todos.push({ id: nanoid(), text, completed: false })
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}),
}))
)
组合中间件
const useStore = create<State>()(
devtools(
persist(
immer((set) => ({
// Store implementation
})),
{ name: 'app-storage' }
),
{ name: 'AppStore' }
)
)
异步操作与 API 集成
基础异步操作
interface UserState {
users: User[]
loading: boolean
error: string | null
fetchUsers: () => Promise<void>
}
const useUserStore = create<UserState>((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null })
try {
const users = await api.getUsers()
set({ users, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
}
},
}))
乐观更新
interface TodoState {
todos: Todo[]
addTodo: (text: string) => Promise<void>
}
const useTodoStore = create<TodoState>((set, get) => ({
todos: [],
addTodo: async (text) => {
const tempId = `temp-${Date.now()}`
const optimisticTodo = { id: tempId, text, completed: false }
// Add optimistically
set((state) => ({ todos: [...state.todos, optimisticTodo] }))
try {
const savedTodo = await api.createTodo(text)
// Replace temp with real todo
set((state) => ({
todos: state.todos.map(t =>
t.id === tempId ? savedTodo : t
)
}))
} catch (error) {
// Rollback on error
set((state) => ({
todos: state.todos.filter(t => t.id !== tempId)
}))
throw error
}
},
}))
请求去重
interface DataState {
data: Data | null
loading: boolean
fetchData: () => Promise<void>
}
let currentRequest: Promise<void> | null = null
const useDataStore = create<DataState>((set) => ({
data: null,
loading: false,
fetchData: async () => {
// Return existing request if in progress
if (currentRequest) return currentRequest
set({ loading: true })
currentRequest = api.getData()
.then((data) => {
set({ data, loading: false })
})
.catch((error) => {
set({ loading: false })
throw error
})
.finally(() => {
currentRequest = null
})
return currentRequest
},
}))
计算值(选择器)
基础选择器
interface TodoState {
todos: Todo[]
}
// Memoized with useCallback or outside component
const selectCompletedCount = (state: TodoState) =>
state.todos.filter(t => t.completed).length
const selectActiveCount = (state: TodoState) =>
state.todos.filter(t => !t.completed).length
function TodoStats() {
const completedCount = useTodoStore(selectCompletedCount)
const activeCount = useTodoStore(selectActiveCount)
return <div>{completedCount} / {activeCount + completedCount}</div>
}
Store 中的派生状态
interface TodoState {
todos: Todo[]
get completed(): Todo[]
get active(): Todo[]
get stats(): { total: number; completed: number; active: number }
}
const useTodoStore = create<TodoState>((set, get) => ({
todos: [],
get completed() {
return get().todos.filter(t => t.completed)
},
get active() {
return get().todos.filter(t => !t.completed)
},
get stats() {
const todos = get().todos
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
}
},
}))
// Usage
const stats = useTodoStore((state) => state.stats)
参数化选择器
// Create selector factory
const selectTodoById = (id: string) => (state: TodoState) =>
state.todos.find(t => t.id === id)
function TodoItem({ id }: { id: string }) {
const todo = useTodoStore(selectTodoById(id))
return <div>{todo?.text}</div>
}
性能优化
订阅模式
// Subscribe to specific state changes
useEffect(() => {
const unsubscribe = useTodoStore.subscribe(
(state) => state.todos,
(todos) => {
console.log('Todos changed:', todos)
}
)
return unsubscribe
}, [])
// Subscribe with selector and equality
const unsubscribe = useTodoStore.subscribe(
(state) => state.todos.length,
(length) => console.log('Todo count:', length),
{ equalityFn: (a, b) => a === b }
)
瞬时更新
// Updates that don't trigger subscribers
interface ScrubbingState {
position: number
updatePosition: (pos: number) => void
}
const useScrubbingStore = create<ScrubbingState>((set) => ({
position: 0,
updatePosition: (pos) => set({ position: pos }, true), // true = transient
}))
// Subscribers won't be notified
useScrubbingStore.getState().updatePosition(50)
批量更新
const useTodoStore = create<TodoState>((set) => ({
todos: [],
batchUpdate: (updates: Partial<TodoState>[]) => {
// Single re-render for multiple updates
set((state) => {
let newState = { ...state }
updates.forEach(update => {
newState = { ...newState, ...update }
})
return newState
})
},
}))
测试策略
Mock Store
// __tests__/Counter.test.tsx
import { create } from 'zustand'
import { render, screen, fireEvent } from '@testing-library/react'
import { Counter } from '@/components/Counter'
import { useCounterStore } from '@/stores/useCounterStore'
// Mock the store
jest.mock('@/stores/useCounterStore')
describe('Counter', () => {
beforeEach(() => {
const mockStore = create<CounterState>((set) => ({
count: 0,
increment: jest.fn(() => set((state) => ({ count: state.count + 1 }))),
decrement: jest.fn(),
}))
useCounterStore.mockImplementation(mockStore)
})
it('increments count', () => {
render(<Counter />)
fireEvent.click(screen.getByText('+'))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})
测试工具
// test-utils.ts
import { create } from 'zustand'
export function createTestStore<T>(initialState: Partial<T>) {
return create<T>(() => initialState as T)
}
// Usage in tests
const testStore = createTestStore<TodoState>({
todos: [
{ id: '1', text: 'Test todo', completed: false }
]
})
在测试之间重置 Store
// stores/useCounterStore.ts
const initialState = { count: 0 }
export const useCounterStore = create<CounterState>((set) => ({
...initialState,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set(initialState),
}))
// __tests__/Counter.test.tsx
afterEach(() => {
useCounterStore.getState().reset()
})
迁移指南
从 Redux 迁移
// Redux
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
},
})
// Zustand equivalent
const useCounterStore = create<CounterState>((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
}))
// Redux usage
const dispatch = useDispatch()
const value = useSelector((state) => state.counter.value)
dispatch(increment())
// Zustand usage
const { value, increment } = useCounterStore()
increment()
从 Context API 迁移
// Context API
const ThemeContext = createContext<ThemeContextType>(null!)
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = () => useContext(ThemeContext)
// Zustand equivalent (no provider needed!)
export const useThemeStore = create<ThemeState>((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}))
// Usage is simpler
const { theme, setTheme } = useThemeStore()
Next.js 集成
App Router(RSC)
// stores/useCartStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useCartStore = create<CartState>()(
persist(
(set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
}),
{
name: 'cart-storage',
// Skip persistence on server
skipHydration: true,
}
)
)
// components/Cart.tsx (Client Component)
'use client'
import { useCartStore } from '@/stores/useCartStore'
import { useEffect } from 'react'
export function Cart() {
const { items, addItem } = useCartStore()
// Hydrate persisted state
useEffect(() => {
useCartStore.persist.rehydrate()
}, [])
return <div>{items.length} items</div>
}
集成 Server Actions
// actions/cart.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function syncCartToServer(items: CartItem[]) {
await db.cart.upsert({
where: { userId: 'current-user' },
update: { items },
create: { userId: 'current-user', items },
})
revalidatePath('/cart')
}
// stores/useCartStore.ts
export const useCartStore = create<CartState>((set) => ({
items: [],
addItem: async (item) => {
set((state) => ({ items: [...state.items, item] }))
// Sync to server
const items = useCartStore.getState().items
await syncCartToServer(items)
},
}))
SSR 水合
// app/layout.tsx
import { CartStoreProvider } from '@/providers/CartStoreProvider'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html>
<body>
<CartStoreProvider>
{children}
</CartStoreProvider>
</body>
</html>
)
}
// providers/CartStoreProvider.tsx
'use client'
import { useRef } from 'react'
import { useCartStore } from '@/stores/useCartStore'
export function CartStoreProvider({ children }: { children: ReactNode }) {
const initialized = useRef(false)
if (!initialized.current) {
// Initialize with server data if needed
useCartStore.setState({ items: [] })
initialized.current = true
}
return <>{children}</>
}
最佳实践
Store 组织方式
// ✅ Good: Single responsibility stores
const useAuthStore = create<AuthState>(...)
const useTodoStore = create<TodoState>(...)
const useUIStore = create<UIState>(...)
// ❌ Bad: God store
const useAppStore = create<AppState>(...)
操作命名
// ✅ Good: Clear, verb-based actions
const useStore = create((set) => ({
addTodo: (text) => set(...),
removeTodo: (id) => set(...),
toggleTodo: (id) => set(...),
}))
// ❌ Bad: Vague or noun-based
const useStore = create((set) => ({
todo: (text) => set(...), // What does this do?
update: (id) => set(...), // Update what?
}))
优化选择器
// ✅ Good: Specific selectors
const user = useStore((state) => state.user)
const theme = useStore((state) => state.theme)
// ❌ Bad: Selecting entire store
const state = useStore() // Re-renders on any change
错误处理
// ✅ Good: Explicit error state
interface State {
data: Data | null
loading: boolean
error: Error | null
fetchData: () => Promise<void>
}
// ❌ Bad: Silent failures
const fetchData = async () => {
try {
const data = await api.getData()
set({ data })
} catch (error) {
// Error silently ignored
}
}
常见模式
加载状态
interface ResourceState<T> {
data: T | null
loading: boolean
error: Error | null
status: 'idle' | 'loading' | 'success' | 'error'
}
function createResourceStore<T>() {
return create<ResourceState<T>>((set) => ({
data: null,
loading: false,
error: null,
status: 'idle',
fetch: async () => {
set({ loading: true, status: 'loading', error: null })
try {
const data = await fetchData()
set({ data, loading: false, status: 'success' })
} catch (error) {
set({ error, loading: false, status: 'error' })
}
},
}))
}
撤销/重做
interface HistoryState<T> {
past: T[]
present: T
future: T[]
set: (state: T) => void
undo: () => void
redo: () => void
}
function createHistoryStore<T>(initialState: T) {
return create<HistoryState<T>>((set) => ({
past: [],
present: initialState,
future: [],
set: (newPresent) => set((state) => ({
past: [...state.past, state.present],
present: newPresent,
future: [],
})),
undo: () => set((state) => {
if (state.past.length === 0) return state
const previous = state.past[state.past.length - 1]
const newPast = state.past.slice(0, -1)
return {
past: newPast,
present: previous,
future: [state.present, ...state.future],
}
}),
redo: () => set((state) => {
if (state.future.length === 0) return state
const next = state.future[0]
const newFuture = state.future.slice(1)
return {
past: [...state.past, state.present],
present: next,
future: newFuture,
}
}),
}))
}
与其他方案的比较
与 Redux 比较
Zustand 的优势:
- 无需样板代码(无需 actions、reducers、dispatch)
- 无需 Provider
- 打包体积更小(约 1kb,而 Redux 约 20kb)
- 异步处理更简单
- 开箱即用的 TypeScript 类型推断
Redux 的优势:
- 时间旅行调试
- 更庞大的生态系统和中间件
- 严格的单向数据流
- 更适合超大型应用
与 Context API 比较
Zustand 的优势:
- 无需陷入 Provider 嵌套
- 性能更好(不会导致整个子树重新渲染)
- API 更简单
- 内置中间件
Context 的优势:
- 内置于 React(无需依赖)
- 更适合组件本地状态
- 组件边界明确
与 Jotai 比较
Zustand 的优势:
- 更传统的基于 Store 的方式
- 更适合复杂状态逻辑
- 从 Redux 迁移更容易
Jotai 的优势:
- 原子化状态管理
- 更利于代码拆分
- 更贴近 React(基于 atom)
- 开箱即用的 Suspense 支持
资源
相关技能
使用 Zustand 时,以下技能可以优化你的工作流:
- react:Zustand Store 的 React 集成模式和 Hook
- tanstack-query:服务端状态管理(与 Zustand 搭配管理客户端状态)
- nextjs:在 Next.js App Router 和 Client Components 中使用 Zustand
- test-driven-development:测试 Zustand Store、操作和选择器
[如果这些技能已部署在你的软件包中,可查阅完整文档]
评论
支持 Markdown