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 编程马·菜鸟教程 版权所有