Skip to content

第14章:基础实战:TodoList(新手入门必做)

TodoList 是 React 新手的经典实战项目,通过实现一个完整的 TodoList 应用,我们可以巩固之前学习的 React 基础知识,包括组件拆分、状态管理、事件处理等。本章将详细介绍如何构建一个功能完整的 TodoList 应用。

14.1 需求分析与页面结构设计

14.1.1 功能需求

  • 添加新任务
  • 标记任务为已完成/未完成
  • 编辑任务内容
  • 删除任务
  • 显示任务总数和已完成任务数
  • 清除已完成任务
  • 本地存储持久化

14.1.2 页面结构设计

TodoList
├── TodoHeader
│   ├── 输入框
│   └── 添加按钮
├── TodoList
│   └── TodoItem (多个)
│       ├── 复选框
│       ├── 任务内容
│       ├── 编辑输入框
│       └── 删除按钮
└── TodoFooter
    ├── 任务统计
    └── 清除已完成按钮

14.2 组件拆分(TodoHeader、TodoList、TodoItem、TodoFooter)

首先,我们需要创建项目结构并拆分组件。

14.2.1 项目结构

src/
├── components/
│   ├── TodoHeader.js
│   ├── TodoList.js
│   ├── TodoItem.js
│   └── TodoFooter.js
├── App.js
└── index.js

14.2.2 创建基础组件

TodoHeader 组件

jsx
// src/components/TodoHeader.js
import React, { useState } from 'react';

function TodoHeader({ onAddTodo }) {
  const [inputValue, setInputValue] = useState('');

  const handleInputChange = (e) => {
    setInputValue(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      onAddTodo(inputValue.trim());
      setInputValue('');
    }
  };

  return (
    <header className="todo-header">
      <h1>Todo List</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={inputValue}
          onChange={handleInputChange}
          placeholder="请输入任务内容"
          className="todo-input"
        />
        <button type="submit" className="add-button">
          添加
        </button>
      </form>
    </header>
  );
}

export default TodoHeader;

TodoItem 组件

jsx
// src/components/TodoItem.js
import React, { useState } from 'react';

function TodoItem({ todo, onToggle, onUpdate, onDelete }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editValue, setEditValue] = useState(todo.text);

  const handleToggle = () => {
    onToggle(todo.id);
  };

  const handleEdit = () => {
    setIsEditing(true);
  };

  const handleEditChange = (e) => {
    setEditValue(e.target.value);
  };

  const handleEditSubmit = (e) => {
    e.preventDefault();
    if (editValue.trim()) {
      onUpdate(todo.id, editValue.trim());
      setIsEditing(false);
    }
  };

  const handleDelete = () => {
    onDelete(todo.id);
  };

  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      {isEditing ? (
        <form onSubmit={handleEditSubmit} className="edit-form">
          <input
            type="text"
            value={editValue}
            onChange={handleEditChange}
            className="edit-input"
            autoFocus
          />
          <button type="submit" className="save-button">
            保存
          </button>
        </form>
      ) : (
        <>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={handleToggle}
            className="todo-checkbox"
          />
          <span className="todo-text" onDoubleClick={handleEdit}>
            {todo.text}
          </span>
          <button onClick={handleDelete} className="delete-button">
            删除
          </button>
        </>
      )}
    </li>
  );
}

export default TodoItem;

TodoList 组件

jsx
// src/components/TodoList.js
import React from 'react';
import TodoItem from './TodoItem';

function TodoList({ todos, onToggle, onUpdate, onDelete }) {
  return (
    <ul className="todo-list">
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onUpdate={onUpdate}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
}

export default TodoList;

TodoFooter 组件

jsx
// src/components/TodoFooter.js
import React from 'react';

function TodoFooter({ todos, onClearCompleted }) {
  const completedCount = todos.filter(todo => todo.completed).length;
  const totalCount = todos.length;

  return (
    <footer className="todo-footer">
      <span className="todo-count">
        已完成 {completedCount} / 总计 {totalCount}
      </span>
      {completedCount > 0 && (
        <button onClick={onClearCompleted} className="clear-button">
          清除已完成
        </button>
      )}
    </footer>
  );
}

export default TodoFooter;

14.3 状态管理(useState 管理任务列表、输入框内容)

现在,我们需要在 App 组件中管理整体状态,包括任务列表的状态。

jsx
// src/App.js
import React, { useState, useEffect } from 'react';
import TodoHeader from './components/TodoHeader';
import TodoList from './components/TodoList';
import TodoFooter from './components/TodoFooter';
import './App.css';

function App() {
  // 从本地存储中加载任务列表
  const [todos, setTodos] = useState(() => {
    const savedTodos = localStorage.getItem('todos');
    return savedTodos ? JSON.parse(savedTodos) : [];
  });

  // 添加新任务
  const handleAddTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text,
      completed: false
    };
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

  // 切换任务完成状态
  const handleToggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // 更新任务内容
  const handleUpdateTodo = (id, text) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, text } : todo
      )
    );
  };

  // 删除任务
  const handleDeleteTodo = (id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  };

  // 清除已完成任务
  const handleClearCompleted = () => {
    setTodos(prevTodos => prevTodos.filter(todo => !todo.completed));
  };

  // 保存任务列表到本地存储
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return (
    <div className="todo-app">
      <TodoHeader onAddTodo={handleAddTodo} />
      <TodoList
        todos={todos}
        onToggle={handleToggleTodo}
        onUpdate={handleUpdateTodo}
        onDelete={handleDeleteTodo}
      />
      <TodoFooter
        todos={todos}
        onClearCompleted={handleClearCompleted}
      />
    </div>
  );
}

export default App;

14.4 功能实现(添加、删除、修改、勾选任务)

14.4.1 添加任务

TodoHeader 组件中,我们通过表单提交来添加新任务。当用户输入任务内容并点击添加按钮时,会调用 onAddTodo 回调函数,将新任务添加到任务列表中。

14.4.2 标记任务完成/未完成

TodoItem 组件中,我们通过复选框的 onChange 事件来切换任务的完成状态。当用户点击复选框时,会调用 onToggle 回调函数,更新对应任务的 completed 属性。

14.4.3 编辑任务

TodoItem 组件中,我们通过双击任务文本进入编辑模式。在编辑模式下,用户可以修改任务内容,然后点击保存按钮或按 Enter 键来保存修改。

14.4.4 删除任务

TodoItem 组件中,我们添加了一个删除按钮。当用户点击删除按钮时,会调用 onDelete 回调函数,从任务列表中移除对应任务。

14.4.5 清除已完成任务

TodoFooter 组件中,我们添加了一个清除已完成按钮。当用户点击该按钮时,会调用 onClearCompleted 回调函数,过滤掉已完成的任务。

14.5 本地存储持久化(localStorage)

为了让任务列表在页面刷新后仍然保持,我们使用了 localStorage 来存储任务列表。

14.5.1 从本地存储加载数据

useState 的初始化函数中,我们从 localStorage 中读取保存的任务列表:

jsx
const [todos, setTodos] = useState(() => {
  const savedTodos = localStorage.getItem('todos');
  return savedTodos ? JSON.parse(savedTodos) : [];
});

14.5.2 保存数据到本地存储

使用 useEffect 钩子,当 todos 状态变化时,将其保存到 localStorage

jsx
useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);

14.6 样式优化(简单CSS美化,提升用户体验)

现在,我们来添加一些 CSS 样式,使 TodoList 应用更加美观。

css
/* src/App.css */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: Arial, sans-serif;
  background-color: #f5f5f5;
  color: #333;
}

.todo-app {
  max-width: 600px;
  margin: 50px auto;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.todo-header {
  padding: 20px;
  background-color: #4CAF50;
  color: white;
}

.todo-header h1 {
  text-align: center;
  margin-bottom: 20px;
}

.todo-input {
  width: 80%;
  padding: 10px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
}

.add-button {
  width: 18%;
  padding: 10px;
  background-color: #2196F3;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  margin-left: 2%;
}

.add-button:hover {
  background-color: #0b7dda;
}

.todo-list {
  list-style: none;
}

.todo-item {
  padding: 15px 20px;
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
  transition: background-color 0.2s;
}

.todo-item:hover {
  background-color: #f9f9f9;
}

.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #999;
}

.todo-checkbox {
  margin-right: 15px;
  width: 18px;
  height: 18px;
}

.todo-text {
  flex: 1;
  font-size: 16px;
  cursor: pointer;
}

.delete-button {
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 5px 10px;
  cursor: pointer;
  font-size: 14px;
}

.delete-button:hover {
  background-color: #d32f2f;
}

.edit-form {
  flex: 1;
  display: flex;
  width: 100%;
}

.edit-input {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.save-button {
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 14px;
  margin-left: 10px;
}

.save-button:hover {
  background-color: #45a049;
}

.todo-footer {
  padding: 15px 20px;
  background-color: #f9f9f9;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-top: 1px solid #eee;
}

.todo-count {
  font-size: 14px;
  color: #666;
}

.clear-button {
  background-color: #ff9800;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 5px 10px;
  cursor: pointer;
  font-size: 14px;
}

.clear-button:hover {
  background-color: #f57c00;
}

/* 响应式设计 */
@media (max-width: 600px) {
  .todo-app {
    margin: 20px;
  }
  
  .todo-input {
    width: 70%;
  }
  
  .add-button {
    width: 28%;
  }
}

14.7 完整代码讲解与优化建议

14.7.1 完整代码结构

我们已经创建了一个完整的 TodoList 应用,包括以下文件:

  1. src/components/TodoHeader.js - 负责输入和添加新任务
  2. src/components/TodoItem.js - 显示单个任务,支持编辑和删除
  3. src/components/TodoList.js - 渲染任务列表
  4. src/components/TodoFooter.js - 显示任务统计和清除已完成任务
  5. src/App.js - 管理整体状态和逻辑
  6. src/App.css - 样式文件

14.7.2 优化建议

  1. 性能优化

    • 使用 React.memo 包装组件,减少不必要的渲染
    • 使用 useCallback 缓存回调函数
  2. 代码组织

    • 将本地存储逻辑提取到自定义 Hook 中
    • 将样式模块化,使用 CSS Modules 或 styled-components
  3. 功能扩展

    • 添加任务过滤功能(全部、已完成、未完成)
    • 添加任务排序功能
    • 添加任务优先级设置
  4. 用户体验

    • 添加任务添加和删除的动画效果
    • 添加键盘快捷键支持
    • 优化编辑模式的用户体验

14.7.3 本地存储 Hook 示例

jsx
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';

export function useLocalStorage(key, initialValue) {
  // 从本地存储中加载数据
  const [value, setValue] = useState(() => {
    try {
      const savedValue = localStorage.getItem(key);
      return savedValue ? JSON.parse(savedValue) : initialValue;
    } catch (error) {
      console.error('Error loading from localStorage:', error);
      return initialValue;
    }
  });

  // 保存数据到本地存储
  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('Error saving to localStorage:', error);
    }
  }, [key, value]);

  return [value, setValue];
}

使用自定义 Hook 优化 App 组件:

jsx
// src/App.js
import React from 'react';
import { useLocalStorage } from './hooks/useLocalStorage';
import TodoHeader from './components/TodoHeader';
import TodoList from './components/TodoList';
import TodoFooter from './components/TodoFooter';
import './App.css';

function App() {
  const [todos, setTodos] = useLocalStorage('todos', []);

  // 其他逻辑保持不变...
}

export default App;

小结

本章通过实现一个完整的 TodoList 应用,我们巩固了以下 React 基础知识:

  • 组件拆分和组合
  • 状态管理(useState)
  • 副作用处理(useEffect)
  • 事件处理
  • 表单操作
  • 本地存储持久化
  • CSS 样式设计

TodoList 是一个很好的入门项目,它涵盖了 React 开发中的许多常见场景。通过这个项目的实践,你应该对 React 的核心概念有了更深入的理解,为后续学习更复杂的 React 应用打下了基础。

© 2026 编程马·菜鸟教程 版权所有