1. 引言:为什么选择 Zustand?
在 React 应用开发中,状态管理是一个绕不开的话题。从早期的 Redux 到后来的 MobX、Recoil,开发者们一直在寻找一个既强大又简洁的解决方案。Zustand(德语意为“状态”)正是在这样的背景下诞生,它以其极简的 API、出色的 TypeScript 支持以及近乎零样板代码的特性,迅速成为 React 社区的新宠。
本文将带你从零开始,系统性地学习 Zustand。你将了解到:
- Zustand 的核心设计哲学与优势
- 如何创建、使用和订阅一个 Store
- 与 React 深度集成的各种模式
- 处理异步操作、持久化、中间件等高级用法
- 通过完整的实战 Demo 巩固所学知识
无论你是 React 新手,还是正在为现有项目的状态管理方案寻找更优解,这篇文章都将为你提供清晰的路径和实用的代码。
2. 环境准备与项目初始化
在开始编写代码之前,我们需要一个 React 开发环境。你可以使用 Vite 或 Create React App 快速搭建。
2.1 创建 React 项目(以 Vite 为例)
打开终端,执行以下命令:
# 使用 npmnpmcreate vite@latest my-zustand-app ----templatereact-ts# 或使用 yarnyarncreate vite my-zustand-app--templatereact-ts# 进入项目目录cdmy-zustand-app2.2 安装 Zustand
在项目根目录下,安装 Zustand 依赖:
npminstallzustand# 或yarnaddzustand现在,你的项目已经准备好了。接下来,让我们深入 Zustand 的核心概念。
3. Zustand 核心概念解析
3.1 Store:状态的单一来源
在 Zustand 中,Store是所有状态和操作逻辑的容器。你可以把它想象成一个自定义的 Hook,但它可以在组件外独立存在和更新。
创建一个 Store 的核心是create函数,它接受一个回调函数,该函数返回一个状态对象。
3.2 State 与 Actions:状态与变更
Zustand 鼓励你将状态(State)和改变状态的方法(Actions)放在同一个 Store 中。这种模式类似于 Vuex 的mutations+actions,但更加直观和一体化。
- State:普通的 JavaScript 对象,存储应用数据。
- Actions:Store 中定义的函数,用于更新 State。它们可以直接通过
set函数修改状态。
3.3 Selector:精准订阅与性能优化
Zustand 的一个巨大优势是选择性订阅。组件可以只订阅 Store 中它关心的那一小部分状态,而不是整个 Store。当其他不相关的状态变化时,该组件不会重新渲染,这极大地提升了性能。
4. 第一个 Zustand Store:计数器示例
让我们从一个经典的计数器开始,创建你的第一个 Store。
4.1 创建 Store 文件
在src目录下创建stores/counterStore.ts:
import{create}from'zustand';// 定义 Store 的状态类型interfaceCounterState{count:number;// Actionsincrement:()=>void;decrement:()=>void;reset:()=>void;incrementByAmount:(amount:number)=>void;}// 使用 create 函数创建 StoreexportconstuseCounterStore=create<CounterState>((set)=>({// 初始状态count:0,// Actions:通过 set 函数更新状态increment:()=>set((state)=>({count:state.count+1})),decrement:()=>set((state)=>({count:state.count-1})),reset:()=>set({count:0}),incrementByAmount:(amount)=>set((state)=>({count:state.count+amount})),}));4.2 在 React 组件中使用
创建一个组件src/components/Counter.tsx:
import React from 'react'; import { useCounterStore } from '../stores/counterStore'; export const Counter: React.FC = () => { // 使用 Hook 获取 Store 中的状态和 Actions const { count, increment, decrement, reset, incrementByAmount } = useCounterStore(); return ( <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}> <h2>计数器 Demo</h2> <p>当前计数: <strong>{count}</strong></p> <div style={{ display: 'flex', gap: '10px', marginTop: '15px' }}> <button onClick={decrement}>-</button> <button onClick={increment}>+</button> <button onClick={() => incrementByAmount(5)}>+5</button> <button onClick={reset}>重置</button> </div> </div> ); };4.3 在主应用中使用
修改src/App.tsx:
import React from 'react'; import './App.css'; import { Counter } from './components/Counter'; function App() { return ( <div className="App"> <h1>Zustand 从零学习 Demo</h1> <Counter /> </div> ); } export default App;运行npm run dev,你将看到一个可以交互的计数器。恭喜你,已经成功创建并使用了第一个 Zustand Store!
5. 进阶用法与模式
5.1 使用 Selector 避免不必要的重渲染
在大型应用中,直接解构整个 Store 可能导致性能问题。我们应该使用 Selector 来精确订阅。
// 低效:订阅了整个 Store,任何状态变化都会导致重渲染 const { count, userName } = useCounterStore(); // 高效:使用 Selector,只订阅 count 状态 const count = useCounterStore((state) => state.count); const increment = useCounterStore((state) => state.increment); // 只有当 count 变化时,这个组件才会重渲染5.2 处理异步 Action
Zustand 处理异步操作非常自然,你可以在 Action 中直接使用async/await。
interfaceUserState{users:User[];loading:boolean;error:string|null;fetchUsers:()=>Promise<void>;}exportconstuseUserStore=create<UserState>((set)=>({users:[],loading:false,error:null,fetchUsers:async()=>{set({loading:true,error:null});try{constresponse=awaitfetch('https://api.example.com/users');constusers=awaitresponse.json();set({users,loading:false});}catch(error){set({error:'获取用户失败',loading:false});}},}));5.3 状态持久化(Persist Middleware)
Zustand 提供了中间件系统。使用persist中间件可以轻松将状态保存到localStorage或sessionStorage。
首先安装中间件包:
npminstallzustand/middleware# 注意:zustand/middleware 是内置的,无需额外安装 persist 包使用示例:
import{create}from'zustand';import{persist}from'zustand/middleware';interfaceSettingsState{theme:'light'|'dark';toggleTheme:()=>void;}exportconstuseSettingsStore=create<SettingsState>()(persist((set)=>({theme:'light',toggleTheme:()=>set((state)=>({theme:state.theme==='light'?'dark':'dark'})),}),{name:'settings-storage',// localStorage 中的 key// 可选:只持久化部分状态// partialize: (state) => ({ theme: state.theme }),}));5.4 在组件外使用 Store
有时你需要在 React 组件之外(例如在工具函数、事件监听器中)读取或修改状态。Zustand Store 本身是一个独立对象,可以直接调用其方法。
// 在非组件环境中import{useCounterStore}from'./stores/counterStore';// 获取当前状态(不会触发订阅)constcurrentCount=useCounterStore.getState().count;// 执行 ActionuseCounterStore.getState().increment();// 监听状态变化(手动管理订阅)constunsubscribe=useCounterStore.subscribe((state)=>state.count,(count)=>{console.log('Count changed to:',count);});// 记得在适当的时候取消订阅// unsubscribe();6. 综合实战:Todo 应用
让我们构建一个更完整的 Todo 应用来巩固所有概念。
6.1 定义 Todo Store
创建src/stores/todoStore.ts:
import{create}from'zustand';exportinterfaceTodo{id:string;text:string;completed:boolean;}interfaceTodoState{todos:Todo[];filter:'all'|'active'|'completed';addTodo:(text:string)=>void;toggleTodo:(id:string)=>void;deleteTodo:(id:string)=>void;setFilter:(filter:'all'|'active'|'completed')=>void;getFilteredTodos:()=>Todo[];}exportconstuseTodoStore=create<TodoState>((set,get)=>({todos:[],filter:'all',addTodo:(text)=>set((state)=>({todos:[...state.todos,{id:Date.now().toString(),text,completed:false}],})),toggleTodo:(id)=>set((state)=>({todos:state.todos.map((todo)=>todo.id===id?{...todo,completed:!todo.completed}:todo),})),deleteTodo:(id)=>set((state)=>({todos:state.todos.filter((todo)=>todo.id!==id),})),setFilter:(filter)=>set({filter}),// 使用 get() 访问当前状态,计算派生状态getFilteredTodos:()=>{const{todos,filter}=get();switch(filter){case'active':returntodos.filter((todo)=>!todo.completed);case'completed':returntodos.filter((todo)=>todo.completed);default:returntodos;}},}));6.2 创建 Todo 组件
创建src/components/TodoApp.tsx:
import React, { useState } from 'react'; import { useTodoStore } from '../stores/todoStore'; import './TodoApp.css'; // 可选的样式文件 export const TodoApp: React.FC = () => { const [inputText, setInputText] = useState(''); const { addTodo, toggleTodo, deleteTodo, setFilter, filter, getFilteredTodos } = useTodoStore(); const todos = useTodoStore(getFilteredTodos); // 使用 Selector 订阅派生状态 const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (inputText.trim()) { addTodo(inputText.trim()); setInputText(''); } }; return ( <div className="todo-container"> <h2>Zustand Todo List</h2> <form onSubmit={handleSubmit}> <input type="text" value={inputText} onChange={(e) => setInputText(e.target.value)} placeholder="输入新的待办事项..." /> <button type="submit">添加</button> </form> <div className="filter-buttons"> {(['all', 'active', 'completed'] as const).map((f) => ( <button key={f} className={filter === f ? 'active' : ''} onClick={() => setFilter(f)} > {f === 'all' ? '全部' : f === 'active' ? '未完成' : '已完成'} </button> ))} </div> <ul className="todo-list"> {todos.map((todo) => ( <li key={todo.id} className={todo.completed ? 'completed' : ''}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> <span>{todo.text}</span> <button onClick={() => deleteTodo(todo.id)}>删除</button> </li> ))} </ul> <p>总计: {todos.length} 项</p> </div> ); };6.3 更新主应用
更新src/App.tsx,集成 Todo 应用:
import React from 'react'; import './App.css'; import { Counter } from './components/Counter'; import { TodoApp } from './components/TodoApp'; function App() { return ( <div className="App"> <h1>Zustand 从零学习 Demo</h1> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px', marginTop: '30px' }}> <Counter /> <TodoApp /> </div> <footer style={{ marginTop: '40px', fontSize: '0.9em', color: '#666' }}> <p>这个 Demo 展示了 Zustand 的核心功能:创建 Store、定义 Actions、使用 Selector、处理派生状态。</p> </footer> </div> ); } export default App;7. 总结与最佳实践
7.1 Zustand 的优势总结
- 极简 API:
create和useStore几乎就是全部,学习成本极低。 - 出色的 TypeScript 支持:类型推断完善,开发体验流畅。
- 高性能:默认支持选择性订阅,自动避免不必要的重渲染。
- 中间件生态:通过官方和社区中间件,轻松实现持久化、日志、Immer 集成等功能。
- 框架无关性:虽然与 React 集成最好,但其核心 Store 可以在任何 JavaScript 环境中使用。
7.2 推荐的项目结构
src/ ├── stores/ │ ├── index.ts // 统一导出所有 Store │ ├── counterStore.ts │ ├── todoStore.ts │ └── userStore.ts ├── components/ │ ├── Counter.tsx │ └── TodoApp.tsx └── App.tsx7.3 下一步学习建议
- 官方文档:Zustand GitHub 是学习的第一站,文档简洁但涵盖所有细节。
- 中间件探索:尝试
zustand/middleware中的devtools(Redux DevTools 集成)、immer(不可变更新)等。 - 状态分片:对于超大型应用,可以考虑按业务域创建多个 Store,而非一个巨型 Store。
- 与 React Query/SWR 结合:Zustand 管理客户端状态,React Query 管理服务器状态,二者是绝配。
8. 常见问题解答(FAQ)
Q: Zustand 和 Redux Toolkit 比,优势在哪?
A: Zustand 的代码量更少,不需要定义slices、reducers、configureStore,概念更简单。对于大多数中小型应用,Zustand 是更轻量、更快速的选择。
Q: 如何在 Class 组件中使用 Zustand?
A: 虽然 Zustand 主要面向函数组件,但你可以通过 HOC 或 Context 将其注入 Class 组件,或者直接使用store.getState()和store.subscribe。
Q: 状态持久化有大小限制吗?
A: 有。localStorage通常有 5-10MB 的限制。对于大量数据,考虑使用indexedDB或服务端存储,并配合persist中间件的自定义存储引擎。
Q: Zustand 适合大型企业级应用吗?
A: 是的。其选择性订阅和组合 Store 的能力足以支撑大型应用。许多知名公司(包括 GitHub 上的大型开源项目)已将其用于生产环境。
希望这篇从零开始的指南能帮助你顺利上手 Zustand。动手实践文中的 Demo,你将会深刻体会到其简洁与强大。Happy coding!