Zustand:轻量级状态管理,从入门到实践
在现代前端开发中,状态管理是一个绕不开的话题。随着应用复杂度的提升,组件间共享状态的需求变得越来越强烈。Redux、MobX 等传统方案虽然强大,但往往伴随着繁琐的样板代码和陡峭的学习曲线。
今天我们要介绍的是 Zustand ------ 一个极简、快速、可扩展的状态管理库。它基于 hooks 思想设计,API 简洁,几乎零样板代码,却能完美应对中小型应用乃至大型项目的状态管理需求。
如果说国家需要有中央银行,那么前端项目就需要中央状态管理系统。Zustand 就是这样一个"中央银行",它将状态集中存储,并提供一套清晰的修改规则,让组件可以轻松共享和操作数据。
本文将通过两个实战案例(计数器、Todo 应用),带你从零掌握 Zustand 的核心用法,并深入理解其高级特性。
第一章:快速上手 Zustand --- 计数器
我们先从一个最简单的计数器开始,感受 Zustand 的简洁与强大。
1. 安装
bash
npm install zustand
# 或
yarn add zustand
2. 创建第一个 store
在 src/store/counter.ts 中:
tsx
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
// 定义状态类型
interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
// 创建 store
export const useCounterStore = create<CounterState>()(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}),
{
name: 'counter', // 持久化存储的 key
}
)
)
代码解读:
create<T>()用于创建 store,返回一个自定义 hook。persist是 Zustand 提供的一个中间件,可以将 store 中的数据自动持久化到 localStorage 中。set函数用于更新状态,可以直接传入新状态,也可以传入一个函数接收当前状态并返回新状态。
3. 在组件中使用
在 App.tsx 中引入并使用:
tsx
import { useCounterStore } from './store/counter'
function App() {
const { count, increment, decrement, reset } = useCounterStore()
return (
<div className="card">
<button onClick={increment}>增加</button>
<span>{count}</span>
<button onClick={decrement}>减少</button>
<button onClick={reset}>重置</button>
</div>
)
}
无需 Provider,无需 Context,直接调用 hook 即可获取状态和操作方法! 这就是 Zustand 的魅力所在。
第二章:管理复杂状态 --- Todo 应用
接下来我们实现一个 Todo 列表,涵盖添加、切换完成状态、删除等功能,并使用 persist 中间件让数据持久化。
1. 定义类型
首先在 src/types/index.ts 中定义 Todo 类型:
ts
export interface Todo {
id: number
text: string
completed: boolean
}
2. 创建 Todo Store
在 src/store/todo.ts 中:
tsx
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { Todo } from '../types'
interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: number) => void
removeTodo: (id: number) => void
}
export const useTodoStore = create<TodoState>()(
persist(
(set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [
...state.todos,
{
id: Date.now(),
text,
completed: false,
},
],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id)
})),
}),
{
name: 'todos', // 持久化 key
}
)
)
3. 在组件中使用
在 App.tsx 中添加 Todo 相关 UI:
tsx
import { useState } from 'react'
import { useTodoStore } from './store/todo'
function App() {
const [inputValue, setInputValue] = useState('')
const { todos, addTodo, toggleTodo, removeTodo } = useTodoStore()
const handleAdd = () => {
if (inputValue.trim() === '') return
addTodo(inputValue)
setInputValue('')
}
return (
<section>
<h2>Todo 列表 ({todos.length})</h2>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
placeholder="输入待办事项"
/>
<button onClick={handleAdd}>添加</button>
</div>
<ul>
{todos.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)}>删除</button>
</li>
))}
</ul>
</section>
)
}
效果 :添加的待办事项会立即显示,切换复选框可以划掉文字,删除按钮可移除对应项。刷新页面后数据依然存在 ------ 因为 persist 中间件已经帮我们同步到了 localStorage。
两个组合在一起的效果图
第三章:组合多个 Store 与性能优化
随着应用规模增长,我们往往会将不同领域的状态拆分到独立的 store 中。Zustand 鼓励这种模式 ------ 每个 store 都是独立的,通过自定义 hook 组合使用。
1. 在组件中同时使用多个 store
tsx
import { useCounterStore } from './store/counter'
import { useTodoStore } from './store/todo'
function App() {
const count = useCounterStore((state) => state.count)
const todos = useTodoStore((state) => state.todos)
// ...
}
2. 使用 selector 优化性能
Zustand 默认会对整个 state 进行浅比较,但如果你的组件只关心部分状态,建议使用 selector 来避免不必要的渲染。
tsx
// 只选取 count,当 count 变化时才重新渲染
const count = useCounterStore((state) => state.count)
// 只选取 increment 函数(函数永远不会变,所以永远不会触发重渲染)
const increment = useCounterStore((state) => state.increment)
3. 创建组合式 Hook
如果多个 store 的状态经常一起使用,可以封装一个自定义 Hook:
tsx
import { useCounterStore } from './counter'
import { useTodoStore } from './todo'
export const useCombinedStore = () => {
const count = useCounterStore((state) => state.count)
const todos = useTodoStore((state) => state.todos)
const addTodo = useTodoStore((state) => state.addTodo)
return { count, todos, addTodo }
}
4. 在 store 中访问其他 store
有时一个 store 的 action 需要依赖另一个 store 的状态。你可以在 action 内部导入并使用其他 store 的 hook(注意避免循环依赖)。
例如,在 todo store 中添加日志,记录当前计数器值:
tsx
import { useCounterStore } from './counter'
export const useTodoStore = create<TodoState>()(
(set, get) => ({
// ...
addTodo: (text) => {
const count = useCounterStore.getState().count
console.log('当前计数器值:', count)
set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }]
}))
},
})
)
第四章:中间件与高级用法
Zustand 提供了多种中间件,除了我们已使用的 persist,还有:
- devtools:与 Redux DevTools 集成,方便调试。
- subscribe:订阅状态变化。
- immer:允许以可变方式编写更新逻辑。
1. 使用 immer 简化更新
安装 immer 相关中间件:
bash
npm install immer
然后修改 store:
tsx
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: number) => void
}
export const useTodoStore = create<TodoState>()(
immer(
persist(
(set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({ id: Date.now(), text, completed: false })
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id)
if (todo) todo.completed = !todo.completed
}),
}),
{ name: 'todos' }
)
)
)
使用 immer 后,你可以像修改可变对象一样更新状态,代码更简洁。
2. 订阅状态变化
Zustand 的 store 本身就是一个可观察对象,你可以通过 subscribe 监听变化:
tsx
const unsubscribe = useTodoStore.subscribe((state, prevState) => {
console.log('todos 从', prevState.todos, '变为', state.todos)
})
// 取消订阅
unsubscribe()
这在需要在状态变化时触发副作用(如埋点、本地存储同步)时非常有用。
总结
通过本文的两个实战案例,我们完整地体验了 Zustand 的核心能力:
- 极简 API:无需繁琐的 action 类型、reducer、provider,几行代码即可创建可全局访问的状态。
- 灵活组合:可以按领域拆分成多个 store,通过 hooks 自由组合。
- 中间件生态:支持持久化、调试、不可变更新等常见需求。
- 完美集成 TypeScript:类型推导自然,开发体验极佳。
相比 Redux,Zustand 的学习成本几乎为零;相比 Context + useReducer,Zustand 避免了 Provider 嵌套和性能问题。它特别适合以下场景:
- 中小型项目,希望快速迭代
- 大型项目,需要清晰的状态边界和性能优化
- 任何需要全局共享状态,但又不想引入复杂概念的项目
希望这篇文章能帮助你快速上手 Zustand,并在实际项目中得心应手。如果你有任何问题或经验分享,欢迎在评论区留言讨论!
