前言:关于“状态管理”的那点破事
兄弟们,咱们做前端开发的,大概率都经历过被 Redux 支配的恐惧。
那是怎样一种体验?想要修改一个 count,你得先写 Action Type,再写 Action Creator,然后跑到 Reducer 里写 switch-case,最后还得在组件里用 connect 或者 useDispatch 连半天。写完这一套,头发都掉两根了,回头一看,我特么只是想让数字 +1 啊!
后来有了 Context API + useReducer,虽然原生了,但性能优化又成了噩梦(Context 一变,全家重渲染)。
这时候,Zustand 骑着一只可爱的小熊来了。
Zustand(德语“状态”)主打一个极简、轻量、Hooks 风格。它不需要你在外面包一层 Provider,也不需要繁琐的模板代码。今天,咱们就结合一个全栈项目(React + TS + Node)的实战场景,从零开始,把 Zustand 玩得明明白白。
一、 起步:为什么是 Zustand?
在我们的全栈规划中,前端部分是 React + TypeScript。如果把组件比作“切图仔”,那状态管理就是“中央银行”。
Zustand 的核心优势就在于:
- 极简主义:API 极少,心智负担几乎为零。
- Hooks 优先:完美契合 React 16.8+ 的开发范式。
- 灵活的中央集权:支持多 Store,也可以单 Store,随你喜欢。
- 不需要 Context Provider:告别组件树顶层那一堆乱七八糟的嵌套。
准备好了吗?咱们直接开整。
环境准备
假设你已经建好了一个 Vite + React + TS 的项目:
npm install zustand
就这一行,完事。
二、 Level 1:从计数器开始(基础用法)
咱们先拿最经典的 Counter 练练手。在 src/store 下新建 counter.ts。
1. 定义状态的“规矩” (Interface)
既然用了 TypeScript,咱们就得讲究点,别满屏 any 乱飞。先定义好咱们的 State 长啥样。
// src/store/counter.ts
import { create } from "zustand";
// 1. 定义接口:状态里有什么,怎么改状态
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
2. 创建仓库 (Create Store)
Zustand 的 create 函数是核心。它接收一个回调,回调里有两个关键参数:set 和 get。
- set: 用于更新状态(你可以把它理解为全局的 setState)。
- get: 用于获取当前状态(偶尔在 action 里需要读取其他状态时用)。
// 2. 创建 Store
export const useCounterStore = create<CounterState>()((set) => ({
count: 0, // 初始值
// 动作 Action:直接写函数
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
// 直接赋值这种写法也行,set 会自动合并第一层属性
reset: () => set({ count: 0 }),
}));
知识点:
Zustand 的 set 默认是合并更新(Merge),不是替换。也就是说,如果你状态里有 a 和 b,你 set({ a: 1 }),b 不会丢。
三、 Level 2:搞定复杂对象(Todo List 实战)
计数器太简单了,咱们来点真实的。Todo 表结构如下:
1. 类型定义
先在 src/types/index.ts 里把类型锁死:
// src/types/index.ts
export interface Todo {
id: number;
text: string;
completed: boolean;
}
2. Todo Store 实现
在 src/store/todo.ts 里,我们要处理数组的操作:增、删、改。这里展示了如何在 Zustand 里优雅地操作不可变数据。
// src/store/todo.ts
import { create } from "zustand";
import type { Todo } from "../types";
export interface TodoState {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
removeTodo: (id: number) => void;
}
export const useTodoStore = create<TodoState>()((set) => ({
todos: [],
// 增:展开旧数组,追加新对象
addTodo: (text: string) =>
set((state) => ({
todos: [
...state.todos,
{
id: +new Date(), // 简单生成个 ID,实际项目请用 uuid 或后端 ID
text,
completed: false,
},
],
})),
// 改:Map 遍历,找到目标取反,其他的保持原样
toggleTodo: (id: number) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
// 删:Filter 过滤
removeTodo: (id: number) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
}));
看看这代码,是不是极其清爽?逻辑和数据在一起,没有 Redux 那种文件跳来跳去的感觉。
3. 在组件中使用
来到 App.tsx,咱们看看怎么用。
// src/App.tsx
import { useState } from "react";
import { useTodoStore } from "./store/todo";
function App() {
// 就像用 useState 一样简单,直接解构
const { todos, addTodo, toggleTodo, removeTodo } = useTodoStore();
const [inputValue, setInputValue] = useState("");
const handleAdd = () => {
if (!inputValue.trim()) return;
addTodo(inputValue);
setInputValue("");
};
return (
<section>
<h2>Todos 数量: {todos.length}</h2>
{/* 输入框区域 */}
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
/>
<button onClick={handleAdd}>Add</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)}>Del</button>
</li>
))}
</ul>
</section>
);
}
export default App;
四、 Level 3:数据持久化(Middleware 中间件)
咱们现在的 Todo List 有个致命弱点:一刷新页面,数据全没了。用户辛辛苦苦写的待办事项瞬间归零,这在生产环境是要被祭天的。
以前咱们得手动 localStorage.setItem,现在 Zustand 自带中间件—— persist。
我们改造一下 todo.ts:
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware"; // 引入中间件
import type { Todo } from "../types";
// ... TodoState 接口保持不变 ...
export const useTodoStore = create<TodoState>()(
// 使用 persist 包裹你的核心逻辑
persist(
(set, get) => ({
todos: [],
addTodo: (text) => set(state => ({ ... })),
toggleTodo: (id) => set(state => ({ ... })),
removeTodo: (id) => set(state => ({ ... })),
}),
{
name: "todos-storage", // 必填:localStorage 里的 key 叫什么
// storage: createJSONStorage(() => sessionStorage), // 选填:默认是 localStorage,你也可以换成 sessionStorage
}
)
);
见证奇迹的时刻:当你写完这段代码,打开浏览器的 Application -> Local Storage,你会发现多了一个 todos-storage。刷新页面,数据依然健在!Zustand 自动帮你处理了序列化和反序列化。
五、 Level 4:异步与用户信息(模拟登录)
在全栈项目中,最常见的场景就是:登录。
这是一个典型的异步操作流程:
- 调用后端 API。
- 等待响应。
- 成功 -> 存 User 信息,改登录状态。
- 失败 -> 报错。
// src/store/user.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { User } from "../types";
interface UserState {
isLoggin: boolean;
userInfo: User | null;
// 这里的 login 支持接收参数
login: (params: { username: string; password: string }) => Promise<void>;
logout: () => void;
}
export const useUserStore = create<UserState>()(
persist(
(set) => ({
isLoggin: false,
userInfo: null,
// 异步 Action:直接写 async/await,毫无黑魔法
login: async ({ username, password }) => {
// 模拟 API 请求
// const res = await axios.post('/api/login', { username, password });
console.log("正在登录...", username);
// 模拟延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟后端返回的用户数据
const mockUser: User = { id: 1, username: username, avatar: "https://i.pravatar.cc/150" };
// 更新状态
set({ isLoggin: true, userInfo: mockUser });
},
logout: () => set({ isLoggin: false, userInfo: null }),
}),
{ name: "user-session" }
)
);
六、 进阶:性能优化
在 App.tsx 中,我们用了这种写法:
// 写法 A (全量订阅)
const { todos, addTodo } = useTodoStore();
这有个小问题:如果 Store 里还有个无关的状态(比如 filterType)变了,虽然 todos 没变,但组件可能会因为 Hook 的返回值变了而重新渲染(Zustand 默认会进行浅比较,通常问题不大,但有极致优化空间)。
专家级写法(Selector 模式) :
如果你想极致优化性能,只订阅你需要的那个切片:
// 写法 B (精确订阅)
const todos = useTodoStore((state) => state.todos);
const addTodo = useTodoStore((state) => state.addTodo);
这样,只有当 state.todos 发生变化时,组件才会重渲染。对于大型列表或复杂页面,这是减少 Render 次数的利器。
此外,Zustand 甚至可以在 React 组件外部使用!
// 在非组件文件中使用(例如 axios 拦截器)
import { useUserStore } from "./store/user";
axios.interceptors.request.use((config) => {
// 直接读取状态,不需要 hooks 规则
const token = useUserStore.getState().userInfo?.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
总结
Zustand 就像一把瑞士军刀,没有多余的装饰,拿起来就能干活。
- 小:包体积极小。
- 快:基于 Flux 模型,性能强悍。
- 强:支持中间件、DevTools、TS 类型推断。
兄弟们,别再犹豫了。如果你的项目还没上 Redux 那些重型武器,或者你正在从 Vue 转 React 寻找那份“可变状态”的亲切感,Zustand 绝对是你的不二之选。