Zustand 状态管理:从入门到实践

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 列表展示区。

[图片:TODO 应用界面截图 - 展示输入框、过滤按钮和 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 组件读取 todosfilter 状态。
  • 它根据 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值