Zustand 状态管理:从入门到实践
Zustand 是一个轻量、快速且灵活的 React 状态管理库。它基于 Hooks API,提供了简洁的接口来创建和使用状态,同时易于扩展和优化。本文将通过一个 TODO 应用实例带你快速入门 Zustand,并探讨其核心概念、性能优化技巧以及一些高级用法。
快速入门:构建一个 TODO 应用
让我们通过实现一个经典的 TODO 应用来熟悉 Zustand 的基本用法。
1. 安装
首先,你需要安装 Zustand:
# 使用 npm
npm install zustand
# 或者使用 yarn
yarn add zustand
2. 创建 Store
接下来,我们使用 Zustand 的 create
函数来定义我们的状态存储(Store)。Store 包含了应用所需的状态以及更新这些状态的方法。
import {
create } from 'zustand';
// 定义 Todo 项的类型 (可选,但推荐使用 TypeScript)
interface Todo {
id: number;
title: string;
completed: boolean;
}
// 定义 Store 的状态和操作类型 (可选)
interface TodoState {
filter: 'all' | 'completed' | 'incompleted';
todos: Todo[];
setFilter: (filter: TodoState['filter']) => void;
setTodos: (fn: (prevTodos: Todo[]) => Todo[]) => void;
// 如果需要添加 Todo 的方法,也可以在这里定义
// addTodo: (title: string) => void;
}
// 用于生成唯一 ID (简化示例)
let keyCount = 0;
const useStore = create<TodoState>((set) => ({
// --- 状态 (State) ---
filter: 'all', // 当前筛选条件,默认为 'all'
todos: [], // Todo 列表,初始为空数组
// --- 操作 (Actions) ---
// 设置筛选条件
setFilter(filter) {
// set 函数用于更新状态,它接收一个对象,包含要更新的部分状态
set({
filter });
},
// 更新 Todo 列表
setTodos(fn) {
// set 函数也可以接收一个函数,参数是当前状态 (prev)
// 这对于基于先前状态进行更新非常有用,可以避免竞态条件
set((prev) => ({
todos: fn(prev.todos) }));
},
// 示例:添加 Todo 的 Action (可以直接在 Store 中定义)
// addTodo(title) {
// set((prev) => ({
// todos: [
// ...prev.todos,
// { title, completed: false, id: keyCount++ },
// ],
// }));
// },
}));
export default useStore;
在这个 Store 中,我们定义了:
filter
: 代表当前的筛选选项(all
- 全部,completed
- 已完成,incompleted
- 未完成)。todos
: 一个数组,存储所有的代办事项对象。每个对象包含id
(唯一标识),title
(事项名称) 和completed
(是否完成)。setFilter
: 一个 Action (操作),用于修改filter
状态。setTodos
: 一个 Action,用于修改todos
状态。它接收一个函数作为参数,该函数接收当前的todos
数组并返回新的todos
数组,确保状态更新的原子性和安全性。
3. 构建 React 组件
现在我们来构建构成 TODO 应用界面的 React 组件。整个应用大致分为三块:输入和过滤控制区、Todo 列表展示区。
App 组件 (入口与表单处理)
App
组件作为应用的根组件,包含添加新 Todo 的表单逻辑。
import React from 'react';
import useStore from './store'; // 引入我们创建的 Store Hook
import Filter from './Filter';
import Filtered from './Filtered';
// 用于生成唯一 ID (简化示例,应与 store 中的 keyCount 保持一致或使用更健壮的 ID 生成方式)
let keyCount = 0;
const App = () => {
// 从 Store 中获取更新 todos 的方法
const { setTodos } = useStore();
// 处理表单提交事件,用于添加新的 Todo
const add = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 阻止表单默认提交行为
const form = e.currentTarget;
const inputElement = form.elements.namedItem('inputTitle') as HTMLInputElement;
const title = inputElement.value.trim(); // 获取输入框的值并去除首尾空格
if (title) { // 确保标题不为空
inputElement.value = ''; // 清空输入框
// 调用 setTodos 更新状态,添加新的 Todo 项
setTodos((prevTodos) => [
...prevTodos,
{ title, completed: false, id: keyCount++ }, // 创建新的 Todo 对象
]);
}
};
return (
<div className="todo-app">
<h1>My Todos</h1>
<form onSubmit={add}>
{/* 过滤组件 */}
<Filter />
{/* 输入框 */}
<input name="inputTitle" placeholder="Add a new todo..." autoComplete="off" />
{/* 列表展示组件 */}
<Filtered />
{/* 隐藏的提交按钮或依赖 Enter 键提交 */}
<button type="submit" style={
{ display: 'none' }}>Add</button>
</form>
</div>
);
};
export default App;
在 App
组件中:
- 我们使用
useStore()
获取setTodos
Action。 add
函数在表单提交时触发(通常是按下 Enter 键),它获取输入框的值,创建一个新的 Todo 对象,并使用setTodos
将其添加到 Store 的todos
数组中,最后清空输入框。
Filter 组件 (过滤选项)
Filter
组件提供单选按钮,让用户可以选择查看所有、已完成或未完成的 Todo。
import React from 'react';
import { Radio } from 'antd'; // 假设使用 antd UI 库
import useStore from './store';
const Filter = () => {
// 从 Store 中获取 filter 状态和 setFilter Action
const { filter, setFilter } = useStore();
return (
<div className="filter-controls">
<Radio.Group onChange={(e) => setFilter(e.target.value)} value={filter}>
<Radio value="all">All</Radio>
<Radio value="completed">Completed</Radio>
<Radio value="incompleted">Incompleted</Radio>
</Radio.Group>
</div>
);
};
export default Filter;
Filter
组件从useStore
获取当前的filter
值和setFilter
Action。- 当用户点击不同的 Radio 按钮时,
onChange
事件触发,调用setFilter
更新 Store 中的filter
状态。
Filtered 组件 (展示过滤后的列表)
Filtered
组件负责根据当前的 filter
状态,从 Store 中获取 todos
列表,进行筛选,并渲染最终的 Todo 列表。这里还使用了 react-spring
来添加简单的动画效果。
import React from 'react';
import { useTransition, animated as a } from '@react-spring/web'; // 引入动画库
import useStore from './store';
import TodoItem from './TodoItem'; // 引入单个 Todo 项组件
const Filtered = () => {
// 从 Store 中获取 todos 列表和 filter 状态
const { todos, filter } = useStore();
// 根据 filter 状态筛选 todos
const filteredTodos = todos.filter((todo) => {
if (filter === 'all') return true; // 显示全部
if (filter === 'completed') return todo.completed; // 显示已完成
return !todo.completed; // 显示未完成 (filter === 'incompleted')
});
// 使用 react-spring 创建列表项的进入/离开动画
const transitions = useTransition(filteredTodos, {
keys: (todo) => todo.id, // 使用 Todo 的唯一 ID 作为 key
from: { opacity: 0, height: 0 },
enter: { opacity: 1, height: 40 }, // 假设每个 Todo 项高度为 40px
leave: { opacity: 0, height: 0 },
config: { tension: 280, friction: 25 } // 动画物理效果配置
});
return (
<div className="todo-list">
{transitions((style, item) => (
// 使用 animated.div 应用动画样式
<a.div className="item" style={style}>
{/* 渲染单个 Todo 项 */}
<TodoItem item={item} />
</a.div>
))}
{filteredTodos.length === 0 && <p className="empty-message">No todos here!</p>}
</div>
);
};
export default Filtered;
Filtered
组件读取todos
和filter
状态。- 它根据
filter
的值计算出filteredTodos
。 useTransition
(来自react-spring
) 用于为列表项添加平滑的进入和离开动画。- 每个筛选后的 Todo 项被传递给
TodoItem
组件进行渲染。
TodoItem 组件 (单个 Todo 项)
TodoItem
组件负责渲染单个 Todo 项,并处理完成状态切换和删除操作。
import React from 'react';
import { CloseOutlined } from '@ant-design/icons'; // 假设使用 antd 图标
import useStore from './store';
import { Todo } from './store'; // 引入 Todo 类型定义
interface TodoItemProps {
item: Todo;
}
const TodoItem = ({ item }: TodoItemProps) => {
// 获取 setTodos Action
const { setTodos } = useStore();
const { title, completed, id } = item;
// 切换 Todo 的完成状态
const toggleCompleted = () =>
setTodos((prevTodos) =>
prevTodos.map((prevItem) =>
prevItem.id === id ? { ...prevItem, completed: !completed } : prevItem,
),
);
// 删除 Todo 项
const remove = () => {
setTodos((prevTodos) => prevTodos.filter((prevItem) => prevItem.id !== id));
};
return (
<>
<input
type="checkbox"
checked={completed}
onChange={toggleCompleted}
aria-label={`Mark ${title} as ${completed ? 'incomplete' : 'complete'}`}
/>
<span style={
{ textDecoration: completed ? 'line-through' : 'none' }}>
{title}
</span>
<CloseOutlined
onClick={remove}
style={
{ cursor: 'pointer', marginLeft: '8px' }}
aria-label={`Remove ${title}`}
/>
</>
);
};
export default TodoItem;
TodoItem
接收一个item
(Todo 对象) 作为 prop。toggleCompleted
函数通过setTodos
更新对应 ID 的 Todo 项的completed
状态。remove
函数通过setTodos
过滤掉当前 ID 的 Todo 项,实现删除。
至此,一个基本的 TODO 应用就完成了。我们通过 create
定义了状态和操作,并通过 useStore
Hook 在组件中访问和更新状态。
使用 Immer 简化嵌套状态更新
当状态结构变得复杂,例如包含多层嵌套对象时,直接使用展开运算符(...
)进行不可变更新会变得非常冗长且容易出错。
考虑以下嵌套状态:
const nestedObject = {
deep: {
nested: {
obj: {
count: 0