从0死磕全栈第五天:React 使用zustand实现To-Do List项目

代码世界是现实的镜像,状态管理教会我们:真正的控制不在于凝固不变,而在于优雅地引导变化。

这是「从0死磕全栈」系列的第5篇文章,前面我们已经完成了环境搭建、路由配置和基础功能开发。今天,我们将引入一个轻量级但强大的状态管理工具 ------ Zustand,来实现一个完整的 TodoList 应用。


Zustand 简介

Zustand 是一个轻量级、灵活的 React 状态管理库,以极简 API 解决复杂状态共享问题。

  • ✅ 无需 Provider 包裹,直接导入使用
  • ✅ 使用 create 创建 store,通过 useStore 随时随地访问状态
  • ✅ 支持中间件(如持久化、日志)、异步逻辑
  • ✅ 代码简洁易维护,是中小型项目的理想选择

下面我们将用 React + TypeScript + Zustand 实现一个功能完整的 TodoList。


要实现的功能

  1. 添加待办事项

    • 在输入框中输入内容,点击"添加"按钮或按 Enter 键
    • 新事项添加到列表底部
  2. 标记完成/未完成

    • 点击复选框切换完成状态
    • 已完成事项显示删除线样式
  3. 删除事项

    • 点击右侧"删除"按钮移除该事项
  4. 统计信息

    • 显示总事项数和已完成事项数

1. 安装依赖

bash 复制代码
npm install zustand

2. 创建 Zustand Store

ts 复制代码
// store/todoStore.ts
import { create } from 'zustand';

// 定义 Todo 项的类型
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

// 定义 Store 的类型
interface TodoStore {
  todos: Todo[];
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  deleteTodo: (id: number) => void;
}

// 创建 Zustand Store
const useTodoStore = create<TodoStore>((set) => ({
  // 初始状态:空数组
  todos: [],

  // 添加新的待办事项
  addTodo: (text: string) =>
    set((state) => ({
      todos: [
        ...state.todos,
        {
          id: Date.now(), // 使用时间戳作为唯一ID
          text,
          completed: false,
        },
      ],
    })),

  // 切换完成状态
  toggleTodo: (id: number) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),

  // 删除待办事项
  deleteTodo: (id: number) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
}));

export default useTodoStore;

💡 设计思想 :Zustand 的 set 函数类似于 Java 中的 setter 方法,提供了统一的状态修改入口,提升了代码的封装性和可读性。


进阶:使用 get 获取当前状态

Zustand 还提供了 get 函数,用于获取当前状态。我们可以扩展 store,添加计算逻辑:

ts 复制代码
// store/todoStore.ts(增强版)
interface TodoStore {
  todos: Todo[];
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  deleteTodo: (id: number) => void;
  getIncompleteCount: () => number; // 新增:获取未完成数量
}

const useTodoStore = create<TodoStore>((set, get) => ({
  todos: [],

  addTodo: (text: string) =>
    set((state) => ({
      todos: [
        ...state.todos,
        {
          id: Date.now(),
          text,
          completed: false,
        },
      ],
    })),

  toggleTodo: (id: number) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),

  deleteTodo: (id: number) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),

  // 使用 get() 获取当前状态并计算
  getIncompleteCount: () => {
    const todos = get().todos;
    return todos.filter((todo) => !todo.completed).length;
  },
}));

3. 创建 TodoList 组件

tsx 复制代码
// components/TodoList.tsx
import { useState } from 'react';
import useTodoStore from '../store/todoStore';

const TodoList = () => {
  // 从 store 中解构状态和方法
  const { todos, addTodo, toggleTodo, deleteTodo } = useTodoStore();
  const [inputValue, setInputValue] = useState('');

  // 添加待办事项
  const handleAddTodo = () => {
    if (inputValue.trim()) {
      addTodo(inputValue);
      setInputValue(''); // 清空输入框
    }
  };

  return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
      <h1>Todo List</h1>

      {/* 输入框和添加按钮 */}
      <div style={{ display: 'flex', marginBottom: '20px' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="输入待办事项"
          style={{ flex: 1, padding: '8px' }}
          onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()}
        />
        <button
          onClick={handleAddTodo}
          style={{ marginLeft: '10px', padding: '8px 16px' }}
        >
          添加
        </button>
      </div>

      {/* 待办事项列表 */}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map((todo) => (
          <li
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              marginBottom: '8px',
              textDecoration: todo.completed ? 'line-through' : 'none',
              color: todo.completed ? '#888' : '#000',
            }}
          >
            {/* 复选框 */}
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
              style={{ marginRight: '10px' }}
            />

            {/* 文本 */}
            <span style={{ flex: 1 }}>{todo.text}</span>

            {/* 删除按钮 */}
            <button
              onClick={() => deleteTodo(todo.id)}
              style={{
                background: '#ff4444',
                color: 'white',
                border: 'none',
                padding: '4px 8px',
                borderRadius: '4px',
              }}
            >
              删除
            </button>
          </li>
        ))}
      </ul>

      {/* 统计信息 */}
      <div style={{ marginTop: '20px', color: '#666' }}>
        总计: {todos.length} 项 | 已完成: {todos.filter(t => t.completed).length} 项
      </div>
    </div>
  );
};

export default TodoList;

4. 创建 App 组件

tsx 复制代码
// App.tsx
import TodoList from './components/TodoList';

function App() {
  return (
    <div className="App">
      <TodoList />
    </div>
  );
}

export default App;

5. 项目结构说明

bash 复制代码
src/
├── store/
│   └── todoStore.ts        # Zustand 状态管理
├── components/
│   └── TodoList.tsx        # 主组件
└── App.tsx                 # 应用入口

6. Zustand 与 useState 对比

✅ 使用 useState 的场景:

  • 管理表单输入、UI 开关状态等组件私有状态
  • 简单的父子组件通信(通过 props 传递)

✅ 使用 Zustand 的场景:

  • 多个无关组件需要共享状态(如全局登录 token、用户信息、主题设置)
  • 状态逻辑复杂,需要集中管理
  • 避免"props drilling"(层层传递 props)

结语:大道至简

Zustand 的哲学很简单 ------ "不要为了状态管理而引入复杂性"

它没有强制架构,没有繁琐规则,只提供最核心的能力:让状态管理变得简单、直观、高效

如果你正在寻找一种"刚刚好"的状态管理方案,Zustand 绝对值得尝试。它可能不会解决所有问题,但一定能让你:

  • ✅ 少写很多代码
  • ✅ 减少嵌套层级
  • ✅ 提升开发愉悦感

关注我,持续更新「从0死磕全栈」系列,带你一步步构建完整的全栈应用。

相关推荐
傻梦兽6 小时前
2025年,跟 encodeURIComponent() 说再见吧
前端·javascript
Lingxing6 小时前
事件流:深入理解事件冒泡、事件捕获与事件委托
前端·javascript·面试
前端小白19956 小时前
面试取经:浏览器篇-跨标签页通信
前端·面试·浏览器
golang学习记6 小时前
从0死磕全栈第4天:使用React useState实现用户注册功能
前端
AlenLi6 小时前
TypeScript - 开发圣经SOLID设计原则
前端·架构
bug_kada6 小时前
深入理解事件捕获与冒泡(详细版)
前端·javascript
wanghao6664556 小时前
如何从chrome中获取会话id
前端·chrome
As33100106 小时前
Chrome 插件开发入门:打造个性化浏览器扩展
前端·chrome