🚀 React 实战:手把手带你写一个功能完备的 Todo List

60 阅读3分钟

🚀 React 实战:手把手带你写一个功能完备的 Todo List

对于 React 初学者来说,Todo List (待办事项清单) 是最经典的入门项目。但写出来和“写得好”是两码事。

今天我们不只是简单的堆砌代码,而是通过一个结构清晰、包含数据持久化(Local Storage)功能的实战案例,来深入理解 组件拆分状态管理 以及 React Hooks 的最佳实践。

📂 项目结构概览

在开始之前,我们先看看组件是如何拆分的。良好的拆分能让代码更易维护。

  • App.jsx: 容器组件。负责管理所有数据状态(State)和核心业务逻辑。
  • TodoInput.jsx: 输入组件。负责收集用户输入。
  • TodoList.jsx: 列表组件。负责展示待办项和操作按钮。
  • TodoStats.jsx: 统计组件。负责展示总数、完成数及清理已完成项。

🛠️ 第一步:核心逻辑与状态管理 (App.jsx)

这是整个应用的“大脑”。我们需要处理增删改查,并且利用 localStorage 防止刷新后数据丢失。

亮点解析:

  1. State 懒初始化:注意 useState 里的函数。我们只在组件首次渲染时读取 localStorage,避免每次渲染都进行 IO 操作,这是一个常见的性能优化点。
  2. 副作用处理:利用 useEffect 监听 todos 变化,自动同步到本地存储。
import './stylus/app.styl';
import TodoList from './components/TodoList';
import TodoInput from './components/TodoInput';
import TodoStats from './components/TodoStats';
import { useEffect, useState } from 'react';

function App() {
  // 1. 初始化状态:从 LocalStorage 读取,如果无数据则为空数组
  // 使用函数式初始化(Lazy Initialization)提升性能
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });

  // 2. 添加逻辑
  const addTodo = (todo) => {
    if (!todo.trim()) return; // 简单的非空校验
    setTodos([...todos, {
      id: Date.now(), // 使用时间戳作为唯一 ID
      todo,
      completed: false,
    }])
  }

  // 3. 删除逻辑
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  // 4. 切换完成状态逻辑
  const toggleTodo = (id) => {
    setTodos(todos.map(todo => todo.id === id ? {
      ...todo,
      completed: !todo.completed,
    } : todo))
  }

  // 5. 清理已完成逻辑
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed))
  }

  // 6. 衍生状态计算 (Derived State)
  // 不需要由于 activeCount 专门定义一个 state,直接计算即可
  const activeCount = todos.filter(todo => !todo.completed).length;
  const completedCount = todos.filter(todo => todo.completed).length;

  // 7. 持久化存储
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos])

  return (
    <div className='todo-app'>
      <h1>My Todo List</h1>
      {/* 传递回调函数给子组件 */}
      <TodoInput onAdd={addTodo}/>
      <TodoList todos={todos} onDelete={deleteTodo} onToggle={toggleTodo}/>
      <TodoStats 
        total={todos.length}
        active={activeCount}
        completed={completedCount}
        onClearCompleted={clearCompleted}
      />
    </div>
  )
}

export default App

📝 第二步:受控组件输入 (TodoInput.jsx)

输入框采用 受控组件 (Controlled Component) 模式,即 input 的 value 由 React 的 state 驱动。

import { useState } from "react";

const TodoInput = (props) => {
    const { onAdd } = props;
    const [inputValue, setInputValue] = useState('');

    const handleSubmit = (e) => {
        e.preventDefault(); // 阻止表单默认提交行为
        onAdd(inputValue);  // 调用父组件传递的方法
        setInputValue('');  // 清空输入框
    }
    
    return (
        <div>
            <form className="todo-input" onSubmit={handleSubmit}>
                <input 
                    type="text"
                    value={inputValue}
                    onChange={e => setInputValue(e.target.value)}
                    placeholder="接下来要做什么?"
                />
                <button>Add</button>
            </form>
        </div>
    )
}

export default TodoInput

📋 第三步:列表渲染与交互 (TodoList.jsx)

这里展示了 React 中最常见的列表渲染模式:map。同时,利用三元运算符处理了“空状态”的展示,提升用户体验。

const TodoList = (props) => {
    const { 
        todos,
        onDelete,
        onToggle
    } = props;
    
    return (
        <div>
            <ul className="todo-list">
                {
                    todos.length === 0 ? (
                        <li className="empty">No todos yet!</li>
                    ) : (
                        todos.map(todo => (
                            <li
                                key={todo.id} // 必须提供唯一的 Key
                                className={todo.completed ? 'completed' : ''}
                            >
                                <label>
                                    <input 
                                        type="checkbox" 
                                        checked={todo.completed}
                                        onChange={() => onToggle(todo.id)}
                                    />
                                    <span>{todo.todo}</span>
                                </label>
                                <button onClick={() => onDelete(todo.id)}>delete</button>
                            </li>
                        ))
                    )
                }
            </ul>
        </div>
    )
}

export default TodoList

📊 第四步:数据统计与操作 (TodoStats.jsx)

这是一个典型的纯展示组件(Presentational Component)。它不持有状态,只通过 props 接收数据并展示,或者触发父组件的回调。

小技巧:利用条件渲染 completed > 0 && (...),只有在有已完成项目时才显示“Clear Completed”按钮,界面更简洁。

const TodoStats = (props) => {
  const {
    total,
    active,
    completed,
    onClearCompleted
  } = props
  
  return (
    <div className="todo-stats">
      <p>Total: {total} | Active: {active} | Completed: {completed}</p>
      {
        completed > 0 && (
          <button 
            onClick={onClearCompleted}
            className="clear-btn"
          >Clear Completed</button>
        )
      }
    </div>
  )
}

export default TodoStats

💡 总结与思考

通过这个简单的 Todo List,我们实践了以下 React 核心概念:

  1. 单向数据流:数据由 App 下发,子组件通过调用 props 中的函数将事件传递回 App
  2. Hooks 组合拳useState 管理状态,useEffect 处理副作用(持久化)。
  3. 组件职责分离:逻辑与视图分离,让代码结构一目了然。