Zustand
Zustand 是一个轻量级的 React 状态管理库,由 React 生态知名开发者创建。
Zustand 学习笔记
1. Zustand 简介
什么是 Zustand?
Zustand 是一个轻量级的 React 状态管理库,由 React 生态知名开发者创建。它的特点是:
- 极简 API(只有一个
create函数) - 无需 Provider 包裹组件
- 支持 Hook 方式使用状态
- 自动处理状态更新和重渲染优化
与其他状态库对比
- 比 Redux 简单很多,减少模板代码
- 比 Context API 更高效,避免不必要的重渲染
- 比 MobX 更轻量,学习成本更低
2. 安装与引入
安装
npm install zustand
# 或
yarn add zustand
基础引入
import { create } from 'zustand'
3. 创建第一个 Store
基础 Store 创建
import { create } from 'zustand'
// 创建 store
const useStore = create((set, get) => ({
// 状态
count: 0,
// Actions:更新状态的方法
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// 异步 action 示例
incrementAsync: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
set((state) => ({ count: state.count + 1 }))
},
// 使用 get 函数访问当前状态
logCount: () => {
const currentCount = get().count
console.log('Current count:', currentCount)
},
}))
4. 在组件中使用 Store
基础使用
import React from 'react'
import useStore from './store'
function Counter() {
// 获取整个 store
const { count, increment, decrement } = useStore()
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
选择性获取状态(性能优化)
function Counter() {
// 只订阅 count 状态,避免不必要的重渲染
const count = useStore((state) => state.count)
const increment = useStore((state) => state.increment)
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>+</button>
</div>
)
}
5. 高级封装模式
分离 State 和 Actions
// store/countStore.js
export const createCountSlice = (set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
})
// store/userStore.js
export const createUserSlice = (set, get) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
})
// store/index.js - 合并多个 slice
import { create } from 'zustand'
import { createCountSlice } from './countStore'
import { createUserSlice } from './userStore'
export const useStore = create((...a) => ({
...createCountSlice(...a),
...createUserSlice(...a),
}))
使用 Immer 处理嵌套状态(可选)
npm install immer
import { produce } from 'immer'
const useStore = create((set) => ({
user: {
profile: {
name: '',
age: 0,
address: {
city: '',
country: '',
},
},
},
// 使用 Immer 简化嵌套更新
updateProfile: (updates) =>
set(
produce((state) => {
state.user.profile = { ...state.user.profile, ...updates }
})
),
updateAddress: (address) =>
set(
produce((state) => {
state.user.profile.address = {
...state.user.profile.address,
...address,
}
})
),
}))
6. 实战示例
Todo 应用示例
// store/todoStore.js
export const useTodoStore = create((set, get) => ({
todos: [],
filter: 'all', // all, active, completed
// Actions
addTodo: (text) =>
set(
produce((state) => {
state.todos.push({
id: Date.now(),
text,
completed: false,
})
})
),
toggleTodo: (id) =>
set(
produce((state) => {
const todo = state.todos.find((todo) => todo.id === id)
if (todo) todo.completed = !todo.completed
})
),
removeTodo: (id) =>
set(
produce((state) => {
state.todos = state.todos.filter((todo) => todo.id !== id)
})
),
setFilter: (filter) => set({ filter }),
// 计算属性
get filteredTodos() {
const { todos, filter } = get()
switch (filter) {
case 'active':
return todos.filter((todo) => !todo.completed)
case 'completed':
return todos.filter((todo) => todo.completed)
default:
return todos
}
},
// 统计
get stats() {
const todos = get().todos
return {
total: todos.length,
completed: todos.filter((t) => t.completed).length,
active: todos.filter((t) => !t.completed).length,
}
},
}))
在组件中使用
function TodoApp() {
const { filteredTodos, addTodo, toggleTodo, removeTodo, setFilter, stats } = useTodoStore()
const [input, setInput] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
if (input.trim()) {
addTodo(input.trim())
setInput('')
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a todo..."
/>
<button type="submit">Add</button>
</form>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
<div>
<p>
Total: {stats.total} | Completed: {stats.completed} | Active: {stats.active}
</p>
</div>
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>
<input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} />
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}
7. 在 Next.js 中使用 Zustand
基础设置(与 React 相同)
// store/store.js
import { create } from 'zustand'
export const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
处理服务端渲染 (SSR)
// 使用 zustand/context 处理 SSR
import { createStore } from 'zustand'
import { createContext } from 'react'
import { useStoreWithEqualityFn } from 'zustand/traditional'
// 创建 store 初始化函数
export const createMyStore = () =>
createStore((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
// 创建 React context
export const StoreContext = createContext(null)
// Provider 组件
export const StoreProvider = ({ children, initialState }) => {
const storeRef = useRef()
if (!storeRef.current) {
storeRef.current = createMyStore(initialState)
}
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>
}
// Hook 用于在组件中访问 store
export const useStore = (selector, equalityFn) => {
const store = useContext(StoreContext)
if (!store) {
throw new Error('Missing StoreProvider')
}
return useStoreWithEqualityFn(store, selector, equalityFn)
}
在 Next.js 页面中使用
// pages/_app.js
import { StoreProvider } from '../store'
export default function App({ Component, pageProps }) {
return (
<StoreProvider>
<Component {...pageProps} />
</StoreProvider>
)
}
// pages/index.js
import { useStore } from '../store'
export default function HomePage() {
const count = useStore(state => state.count)
const increment = useStore(state => state.increment)
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
</div>
)
}
处理 Hydration 问题
// 解决 SSR hydration 不匹配问题
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
export const useStore = create(
persist(
(set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'app-storage', // 存储名称
storage: createJSONStorage(() => localStorage), // 或 sessionStorage
// 在 Next.js 中,你可能需要自定义存储以避免 SSR 问题
// onRehydrateStorage: () => (state) => {
// console.log('hydration complete')
// }
}
)
)
8. 性能优化技巧
1. 选择性订阅
// 不好:组件会在任何状态变化时重渲染
const { count, user } = useStore()
// 好:只在 count 变化时重渲染
const count = useStore((state) => state.count)
// 好:使用 shallow 比较对象
import { shallow } from 'zustand/shallow'
const { user, profile } = useStore(
(state) => ({ user: state.user, profile: state.profile }),
shallow
)
2. 使用 Derive 状态
const useStore = create((set, get) => ({
items: [],
filter: '',
// 使用 getter 计算派生状态
get filteredItems() {
const { items, filter } = get()
return items.filter((item) => item.includes(filter))
},
}))
3. 批量更新
// 多个状态更新合并为一次重渲染
const useStore = create((set) => ({
user: null,
profile: null,
loading: false,
// 批量更新
setUserData: (user, profile) => set({ user, profile }),
// 或者使用函数式更新
updateMultiple: () =>
set((state) => ({
user: { ...state.user, name: 'New Name' },
profile: { ...state.profile, age: 30 },
})),
}))
9. 调试与开发工具
安装开发工具
npm install @redux-devtools/extension
启用 DevTools
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools(
(set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'MyStore' }
)
)
10. 学习笔记文档
Zustand 学习笔记
核心概念
1. Store 创建
- 使用
create函数创建 store - 接收 set、get 参数用于更新和访问状态
- 返回一个 hook 用于在组件中使用
2. 状态更新
set函数用于更新状态- 支持直接设置值或函数式更新
- 自动合并浅层状态(深层需要手动处理或使用 Immer)
3. 组件使用
- 使用生成的 hook 访问 store
- 可以通过 selector 函数选择性订阅状态
- 使用
shallow比较优化对象/数组的重新渲染
最佳实践
1. 组织代码
- 按功能模块拆分 store
- 使用 slice 模式组合相关状态
- 将复杂逻辑提取到自定义 hooks
2. 性能优化
- 总是使用选择性订阅
- 对对象/数组使用浅比较
- 使用派生状态避免重复计算
3. 类型安全(TypeScript)
interface StoreState {
count: number
increment: () => void
decrement: () => void
}
const useStore = create<StoreState>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
常见问题解决
1. 循环依赖
- 避免在 store 中直接导入组件
- 使用函数参数或回调解决依赖问题
2. 测试
// 测试 store
import { act } from '@testing-library/react'
import { useStore } from './store'
test('increment increases count by 1', () => {
const { increment, getState } = useStore
act(() => {
increment()
})
expect(getState().count).toBe(1)
})
3. 服务端渲染
- 使用 zustand/context 处理 SSR
- 注意 hydration 不匹配问题
- 考虑使用 persist middleware 处理状态持久化
3. 常见坑
- 创建 create 对象要注意它本身就是个 hook,因此在抛出的时候要不就直接抛出,要不就设定成一个回调函数,不然会报错,因为 hook 不可以在顶层使用
- 在 Server Component 调用 useStore:不允许,必须在 Client Component 使用。
- persist 与 SSR:localStorage 仅在浏览器;不要在服务端触发持久化。
- 跨请求状态泄漏:服务端不要持久化修改单例 store;使用“数据注水”而不是共享实例。
- 选择器返回新对象:每次返回新引用会导致重渲;配合 shallow 或只订原子字段。