React 状态管理:Zustand 完全指南
本教程面向 React 初学者与前端进阶开发者,用生活化的语言讲解 Zustand 的核心概念、使用方法和常见踩坑点。所有代码示例均可直接运行。
一、Zustand 是什么?
1.1 一句话定义
Zustand(德语:状态)是一个极简、零依赖的 React 状态管理库。
它允许你用很少的代码创建一个可全局访问的“状态仓库”(Store),并在任意 React 组件中读取和更新状态,不需要像 Redux 那样写模板代码,也不需要像 Context 那样包裹 Provider。
1.2 为什么要用 Zustand?先对比其他方案
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| useState | 简单、React 内置 | 只能本组件使用,跨组件传递麻烦 | 单个组件内部状态 |
| Context | 官方方案,可跨层级传递 | 任一值变化会导致所有消费者重渲染;Provider 嵌套地狱 | 主题、语言、用户身份等低频变化的全局数据 |
| Redux | 生态强大、可预测 | 样板代码多、学习曲线陡、需要写 action/reducer | 超大型应用、复杂状态流转 |
| MobX | 响应式、代码少 | 语法有一定魔法感,调试需要适应 | 中大型应用 |
| Zustand | 极简 API、无 Provider、按需订阅、最小重渲染 | 生态比 Redux 小(但常用插件齐全) | 小到中大型 React 应用 |
生活中的类比:
useState就像你的手机备忘录,只能自己看、自己改。Context像班级公告栏,但是一更新公告,全班所有人都要站起来看一遍,哪怕内容和自己没关系。Redux像一套完整的办公流程:写申请表、找领导签字、由专人修改档案。流程规范,但琐事多。- Zustand 像公司里的共享文件柜:每个员工拿一把钥匙,谁要拿什么文件自己取,只会在自己需要的文件更新时收到通知。
1.3 Zustand 的核心优势
- 极简 API:创建 Store 只需一行
create(),就能同时得到状态和方法。 - 最小重渲染:组件只订阅自己需要的状态片段,其他状态变化不会触发重渲染。
- 跨组件 / 无组件访问:Store 是一个普通对象,不仅组件能用,也可以在工具函数、事件回调中调用。
- 无 Provider 嵌套:不需要在
App根节点包裹 Provider,组件树保持干净。 - TypeScript 友好:天然类型推导,无需额外配置。
1.4 底层运行原理说明
Zustand 的运行可以简化成三个步骤:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 创建 Store │ ──▶ │ 组件订阅状态 │ ──▶ │ 状态更新通知 │
│ create() 函数 │ │ useXxxStore() │ │ 订阅器重新渲染 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Store 内部大致结构:
- 一个保存当前状态的 JavaScript 对象。
- 一个
set函数,用来更新状态(并通知订阅者)。 - 一个
get函数,用来读取当前状态。 - 一个订阅者列表,记录哪些组件正在监听哪些状态。
当调用 set 更新状态时,Zustand 会检查每个订阅者关心的状态是否发生变化。如果变化了,就触发该组件重渲染;否则跳过。这保证了只有“相关”的组件才会更新,而不是全局一起刷新。
二、Zustand 完整使用流程
2.1 环境搭建:安装 Zustand
Zustand 支持 npm、yarn、pnpm 三种常见包管理器:
# npm
npm install zustand
# yarn
yarn add zustand
# pnpm
pnpm add zustand
同时确保你的 React 项目已创建(例如使用 Vite):
npm create vite@latest zustand-demo -- --template react
如果你使用 TypeScript,选择
react-ts模板即可,Zustand 对 TS 支持非常友好。
2.2 基础 Store:计数器
在项目中新建文件 store.js(或 store.ts):
// store.js
import { create } from "zustand";
export const useCounterStore = create((set) => ({
// 1. 状态
count: 0,
// 2. 修改状态的方法
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
API 拆解:
| 代码 | 含义 |
|---|---|
create() |
创建 Store 的核心函数,接收一个函数作为参数。 |
(set) => ({ ... }) |
参数函数接收 set 和 get,返回一个对象,对象里的属性就是状态和方法。 |
set(...) |
更新状态。可以直接传新对象,也可以传函数基于旧状态更新。 |
useCounterStore |
自定义 Hook,可以在组件中调用它来读取状态。 |
为什么 set 推荐用函数?
因为 set((state) => ({ count: state.count + 1 })) 是基于当前最新状态更新的,可以避免并发更新时的覆盖问题。就像排队买票时,售票员根据当前余票数量再决定卖不卖,而不是直接按你最初看到的数量处理。
2.3 在组件中使用 Store
场景 A:读取状态并显示
// Display.jsx
import { useCounterStore } from "./store";
function Display() {
const count = useCounterStore((state) => state.count);
return <h1>当前计数:{count}</h1>;
}
export default Display;
useCounterStore((state) => state.count) 的意思是:我只关心 count 这个字段。只有当 count 变化时,这个组件才会重新渲染。
场景 B:调用 Store 方法
// Controls.jsx
import { useCounterStore } from "./store";
function Controls() {
// 同时读取多个方法,可以一次解构
const { increase, decrease, reset } = useCounterStore((state) => ({
increase: state.increase,
decrease: state.decrease,
reset: state.reset,
}));
return (
<div>
<button onClick={increase}>+1</button>
<button onClick={decrease}>-1</button>
<button onClick={reset}>重置</button>
</div>
);
}
export default Controls;
场景 C:合并展示
// App.jsx
import Display from "./Display";
import Controls from "./Controls";
function App() {
return (
<div style={{ padding: 20 }}>
<Display />
<Controls />
</div>
);
}
export default App;
到这里,一个最基础的 Zustand 应用就搭建完成了。它比 useState 多一步创建 Store,但换来的是跨组件自由共享状态的能力。
三、Zustand 进阶用法
3.1 分片读取状态:优化重渲染
Zustand 最强大的特性之一就是精细化订阅。
// Bad:直接解构整个 Store
function UserCard() {
const { name, age, city } = useUserStore(); // 任一状态变化都会触发重渲染
return (
<div>
<p>{name}</p>
<p>{age}</p>
</div>
);
}
// Good:只订阅需要的字段
function UserCard() {
const name = useUserStore((state) => state.name);
const age = useUserStore((state) => state.age);
return (
<div>
<p>{name}</p>
<p>{age}</p>
</div>
);
}
业务价值:在表单、列表、仪表盘等场景中,避免一个字段更新导致整个页面重绘,大幅提升性能。
3.2 中间件
Zustand 的中间件可以扩展 Store 能力。下面介绍三个最常用的。
3.2.1 persist:状态持久化
场景:用户登录 token、主题设置、购物车数据需要在刷新页面后保留。
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const useThemeStore = create(
persist(
(set) => ({
theme: "light",
toggleTheme: () =>
set((state) => ({
theme: state.theme === "light" ? "dark" : "light",
})),
}),
{
name: "theme-storage", // localStorage 的 key
},
),
);
这样,theme 会自动同步到 localStorage,刷新页面后仍然保留。
3.2.2 devtools:Redux DevTools 调试
场景:开发阶段追踪状态变化,排查 bug。
import { create } from "zustand";
import { devtools } from "zustand/middleware";
export const useCounterStore = create(
devtools(
(set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: "CounterStore" },
),
);
安装浏览器 Redux DevTools 扩展后,即可在面板中查看每个状态变更。
3.2.3 immer:像修改普通对象一样更新状态
场景:嵌套对象或数组更新时,不用手动展开所有层级。
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
export const useCartStore = create(
immer((set) => ({
items: [],
addItem: (product) =>
set((state) => {
state.items.push(product); // 直接修改,immer 会自动生成不可变快照
}),
removeItem: (id) =>
set((state) => {
state.items = state.items.filter((item) => item.id !== id);
}),
})),
);
业务价值:在处理表单、嵌套配置、购物车等复杂数据结构时,immer 让代码可读性接近普通对象操作,同时保持 React 所需的不可变更新语义。
3.3 Store 的拆分与组合
大型项目不要写成一个万能 Store,而是按业务模块拆分,再组合使用。
// userStore.js
import { create } from "zustand";
export const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));
// cartStore.js
import { create } from "zustand";
export const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));
// App.jsx 中按需组合使用
import { useUserStore } from "./userStore";
import { useCartStore } from "./cartStore";
function Header() {
const user = useUserStore((state) => state.user);
const count = useCartStore((state) => state.items.length);
return (
<header>
<span>欢迎,{user?.name ?? "游客"}</span>
<span>购物车({count})</span>
</header>
);
}
业务价值:每个 Store 职责单一,便于维护、测试和团队协作。
3.4 在 React 组件外调用 Store
Zustand 的 Store 是一个普通对象,可以直接调用方法,这在事件处理、工具函数、非 React 代码中非常有用。
import { useCounterStore } from "./store";
// 普通函数中直接调用
function someUtility() {
useCounterStore.getState().increase();
console.log(useCounterStore.getState().count);
}
// 在 API 请求拦截器中重置登录状态
apiClient.interceptors.response.use(
(res) => res,
(error) => {
if (error.response?.status === 401) {
useUserStore.getState().logout();
}
return Promise.reject(error);
},
);
业务价值:在路由守卫、API 拦截器、定时任务、原生 DOM 事件等场景中,无需把 Store 注入组件也能操作状态。
四、开发注意事项与常见踩坑
4.1 直接解构 Store 导致全量重渲染
错误写法:
function Profile() {
const { name, age, city } = useUserStore(); // 订阅了全部状态
return <p>{name}</p>;
}
问题:只要 age 或 city 变化,即使 Profile 只显示 name,组件也会重新渲染。
正确写法:
function Profile() {
const name = useUserStore((state) => state.name);
return <p>{name}</p>;
}
4.2 异步更新状态的常见错误
错误写法:
const useUserStore = create((set) => ({
user: null,
fetchUser: async () => {
const res = await fetch("/api/user");
const user = await res.json();
set({ user }); // 正确
},
}));
上面的写法本身没问题,但初学者容易犯的错误是:
// 错误:在 async 函数里直接 return 数据,没有 set
fetchUser: async () => {
const res = await fetch("/api/user");
return await res.json(); // 只是返回了数据,状态没有更新!
};
正确写法:异步方法里必须调用 set 才能更新状态。
fetchUser: async () => {
set({ user: null }); // 先清空,显示 loading
const res = await fetch("/api/user");
const user = await res.json();
set({ user });
};
组件中:
const user = useUserStore((state) => state.user);
const fetchUser = useUserStore((state) => state.fetchUser);
if (!user) return <button onClick={fetchUser}>加载用户</button>;
return <p>{user.name}</p>;
4.3 持久化存储的版本兼容问题
错误场景:产品上线后,你发现 user 结构变了,但用户本地还保存着旧版数据。
const useUserStore = create(
persist(
(set) => ({
user: { name: "", age: 0 }, // 新版字段变了
}),
{ name: "user-storage" },
),
);
问题:旧版 localStorage 里可能存的是 { name: 'Tom' },新版读取后会缺少 age,甚至出现 undefined 错误。
正确写法:使用 version 和 migrate 字段处理迁移。
const useUserStore = create(
persist(
(set) => ({
user: { name: "", age: 0 },
}),
{
name: "user-storage",
version: 2, // 版本号升级
migrate: (persistedState, version) => {
if (version === 1) {
persistedState.user.age = persistedState.user.age ?? 0;
}
return persistedState;
},
},
),
);
4.4 SSR 场景下的状态同步问题
错误场景:在 Next.js 等 SSR 框架中,服务端渲染的 HTML 和客户端 hydration 状态不一致,出现闪烁或报错。
原因:服务端没有 localStorage,persist 默认在服务端不会生效; hydration 时客户端状态可能和服务器 HTML 不一致。
正确写法:用 skipHydration 或自定义 storage 控制 hydration。
import { persist, createJSONStorage } from "zustand/middleware";
const useUserStore = create(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
}),
{
name: "user-storage",
storage: createJSONStorage(() => localStorage),
skipHydration: true,
},
),
);
同时在页面中手动触发 hydration:
useEffect(() => {
useUserStore.persist.rehydrate();
}, []);
4.5 大型项目中的 Store 拆分规范
错误示范:一个 Store 包含所有业务状态。
const useAppStore = create((set) => ({
user: null,
cart: [],
orders: [],
settings: {},
// ... 所有状态都塞在这里
}));
问题:文件臃肿、职责混乱、多人协作容易冲突、难以复用和测试。
正确规范:
- 按业务模块拆分:
userStore.js、cartStore.js、orderStore.js。 - Store 命名统一:
useXxxStore,文件名为xxxStore.js。 - 状态和方法放在一起:与该模块相关的所有状态、异步请求、计算操作都放到同一个 Store。
- 跨 Store 通信优先使用方法调用:例如
orderStore提交订单后清空购物车,可以直接调用useCartStore.getState().clear()。
// orderStore.js
import { create } from "zustand";
import { useCartStore } from "./cartStore";
export const useOrderStore = create((set) => ({
orders: [],
submitOrder: async () => {
const items = useCartStore.getState().items;
const res = await fetch("/api/order", {
method: "POST",
body: JSON.stringify({ items }),
});
const order = await res.json();
set((state) => ({ orders: [...state.orders, order] }));
useCartStore.getState().clear(); // 清空购物车
},
}));
五、实战练习题
练习 1:基础计数器(入门)
目标:实现一个可以加减的计数器,并且限制最小值为 0,最大值为 10。
要求:
- 使用 Zustand 创建 Store。
- 提供
increase、decrease、reset三个方法。 - 当
count达到 0 时,decrease无效;当达到 10 时,increase无效。
练习 2:待办事项列表(进阶)
目标:实现一个 Todo List。
要求:
- Store 包含
todos数组和filter字段。 - 提供
addTodo(text)、toggleTodo(id)、removeTodo(id)、setFilter(filter)方法。 - 组件中展示过滤后的列表(全部 / 已完成 / 未完成)。
- 使用
persist中间件持久化到localStorage。
练习 3:用户信息跨页面持久化(综合)
目标:模拟一个登录系统,用户信息在多个页面共享并持久化。
要求:
- 创建
useUserStore,包含user({ name, role })和login(name, role)、logout()方法。 - 使用
persist持久化,刷新页面后仍然保持登录状态。 - 在登录页输入用户名和角色,登录后跳转到首页展示欢迎信息。
- 提供退出登录按钮,点击后清空状态并返回登录页。
六、参考答案与思路解析
练习 1 参考答案
// store.js
import { create } from "zustand";
export const useCounterStore = create((set) => ({
count: 0,
increase: () =>
set((state) => ({
count: Math.min(state.count + 1, 10),
})),
decrease: () =>
set((state) => ({
count: Math.max(state.count - 1, 0),
})),
reset: () => set({ count: 0 }),
}));
// App.jsx
import { useCounterStore } from "./store";
function App() {
const count = useCounterStore((state) => state.count);
const { increase, decrease, reset } = useCounterStore((state) => ({
increase: state.increase,
decrease: state.decrease,
reset: state.reset,
}));
return (
<div style={{ padding: 20 }}>
<h1>计数器:{count}</h1>
<button onClick={increase}>+1</button>
<button onClick={decrease}>-1</button>
<button onClick={reset}>重置</button>
</div>
);
}
export default App;
思路:在 set 函数里用 Math.min / Math.max 限制范围,确保不会越界。
练习 2 参考答案
// store.js
import { create } from "zustand";
import { persist } from "zustand/middleware";
let nextId = 1;
export const useTodoStore = create(
persist(
(set) => ({
todos: [],
filter: "all", // all | completed | active
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: nextId++, 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),
})),
setFilter: (filter) => set({ filter }),
}),
{ name: "todo-storage" },
),
);
// App.jsx
import { useMemo } from "react";
import { useTodoStore } from "./store";
function App() {
const todos = useTodoStore((state) => state.todos);
const filter = useTodoStore((state) => state.filter);
const { addTodo, toggleTodo, removeTodo, setFilter } = useTodoStore(
(state) => ({
addTodo: state.addTodo,
toggleTodo: state.toggleTodo,
removeTodo: state.removeTodo,
setFilter: state.setFilter,
}),
);
const filteredTodos = useMemo(() => {
if (filter === "completed") return todos.filter((t) => t.completed);
if (filter === "active") return todos.filter((t) => !t.completed);
return todos;
}, [todos, filter]);
const handleAdd = (e) => {
e.preventDefault();
const text = e.target.text.value.trim();
if (!text) return;
addTodo(text);
e.target.reset();
};
return (
<div style={{ padding: 20 }}>
<h1>待办事项</h1>
<form onSubmit={handleAdd}>
<input name="text" placeholder="输入事项" />
<button type="submit">添加</button>
</form>
<div>
<button onClick={() => setFilter("all")}>全部</button>
<button onClick={() => setFilter("active")}>未完成</button>
<button onClick={() => setFilter("completed")}>已完成</button>
</div>
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}
export default App;
思路:用 filter 控制展示状态,用 useMemo 过滤列表避免重复计算。persist 让数据刷新不丢失。
练习 3 参考答案
// userStore.js
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const useUserStore = create(
persist(
(set) => ({
user: null,
login: (name, role) => set({ user: { name, role } }),
logout: () => set({ user: null }),
}),
{ name: "user-storage" },
),
);
// Login.jsx
import { useUserStore } from "./userStore";
function Login({ onLogin }) {
const login = useUserStore((state) => state.login);
const handleSubmit = (e) => {
e.preventDefault();
const name = e.target.name.value;
const role = e.target.role.value;
login(name, role);
onLogin();
};
return (
<form onSubmit={handleSubmit}>
<h2>登录</h2>
<input name="name" placeholder="用户名" required />
<select name="role" required>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
</select>
<button type="submit">登录</button>
</form>
);
}
export default Login;
// Home.jsx
import { useUserStore } from "./userStore";
function Home({ onLogout }) {
const user = useUserStore((state) => state.user);
const logout = useUserStore((state) => state.logout);
const handleLogout = () => {
logout();
onLogout();
};
return (
<div>
<h2>欢迎,{user.name}!</h2>
<p>你的角色是:{user.role}</p>
<button onClick={handleLogout}>退出登录</button>
</div>
);
}
export default Home;
// App.jsx
import { useState } from "react";
import Login from "./Login";
import Home from "./Home";
import { useUserStore } from "./userStore";
function App() {
const user = useUserStore((state) => state.user);
const [view, setView] = useState(user ? "home" : "login");
return (
<div style={{ padding: 20 }}>
{view === "login" ? (
<Login onLogin={() => setView("home")} />
) : (
<Home onLogout={() => setView("login")} />
)}
</div>
);
}
export default App;
思路:Store 负责“有没有用户”这个状态,组件只负责视图切换。persist 让登录状态在页面刷新后保留。实际项目中,view 状态可以用 React Router 路由替代,这里为了简化用 useState 控制。
总结
Zustand 是一个把“简单”做到极致的 React 状态管理库。它的核心思想是:
- 用
create创建一个全局 Store。 - 用选择器函数
(state) => state.xxx精确订阅需要的状态。 - 用中间件扩展持久化、调试、不可变更新等能力。
- 按业务模块拆分 Store,保持项目清晰。
掌握这些要点后,你就已经能够在中大型 React 项目中熟练使用 Zustand 了。课后请完成三个练习题,并尝试把 persist 和 immer 组合使用,看看会有怎样的效果!