React Todo 应用学习指南

49 阅读4分钟

前言

在现代前端开发中,React 已成为构建用户界面的主流框架之一。通过组件化和状态管理,开发者可以轻松创建高效、可维护的应用程序。本文将以一个完整的 Todo 应用 为例,详细介绍如何使用 React 实现父子组件通信、状态管理以及事件处理等核心功能。我们将从项目结构入手,逐步解析每个组件的功能,并提供一些补充知识帮助你更好地理解。


一、项目概述

1.1 功能需求

  • 添加待办事项:用户可以通过输入框添加新的待办事项。
  • 删除待办事项:用户可以删除已有的待办事项。
  • 标记完成:用户可以通过勾选复选框标记待办事项为已完成或未完成。
  • 清除已完成项:用户可以一键清除所有已完成的待办事项。

1.2 技术栈

  • React: 主要框架,用于构建用户界面。
  • Stylus: CSS 预处理器,简化样式编写。
  • Vite: 构建工具,支持快速开发和热更新。

二、项目结构


src/
├── components/
│   ├── TodoInput.jsx    // 输入框组件
│   ├── TodoList.jsx     // 待办列表组件
│   └── TodoStats.jsx    // 状态统计组件
├── styles/
│   └── app.styl         // 全局样式
└── App.jsx              // 根组件

三、组件详解

3.1 TodoInput 组件

功能描述

  • 提供一个输入框,允许用户输入待办事项。
  • 当用户点击“ADD”按钮时,触发 onAdd 回调函数,将输入的内容传递给父组件。

代码实现

import { useState } from 'react';

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

    const handleSubmit = (e) => {
        e.preventDefault();
        if (inputValue.trim() === '') return; // 可选:防止空内容
        onAdd(inputValue);
        setInputValue(''); // 清空输入框
    };

    return (
        <form className="todo-input" onSubmit={handleSubmit}>
            <input
                type="text"
                value={inputValue}
                onChange={(e) => setInputValue(e.target.value)}
                placeholder="输入待办事项..."
            />
            <button type="submit">ADD</button>
        </form>
    );
};

export default TodoInput;

关键点解析

  • useState: 用于管理输入框的状态。
  • handleSubmit: 表单提交时阻止默认行为,并调用 onAdd 回调函数传递输入值。

3.2 TodoList 组件

功能描述

  • 展示所有待办事项的列表。
  • 每个待办事项包含一个复选框(用于标记完成/未完成)和一个删除按钮。

代码实现

const TodoList = (props) => {
    const { todos, onDelete, onToggle } = props;

    return (
        <ul className="todo-list">
            {todos.length === 0 ? (
                <li className="empty">No todos yet!</li>
            ) : (
                todos.map((todo) => (
                    <li key={todo.id} className={todo.completed ? 'completed' : ''}>
                        <label>
                            <input
                                type="checkbox"
                                checked={todo.completed}
                                onChange={() => onToggle(todo.id)}
                            />
                            <span>
                                {todo.text}
                            </span>
                        </label>
                        <button onClick={() => onDelete(todo.id)}>x</button>
                    </li>
                ))
            )}
        </ul>
    );
};

export default TodoList;

关键点解析

  • map: 遍历 todos 数组,生成对应的 <li> 元素。
  • onDelete 和 onToggle: 分别用于删除和切换待办事项的状态。

3.3 TodoStats 组件

功能描述

  • 显示待办事项的总数、活跃数和已完成数。
  • 提供一个按钮用于清除所有已完成的待办事项。

代码实现

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;

关键点解析

  • 条件渲染: 当 completed 大于 0 时,才显示“Clear Completed”按钮。

3.4 App 组件

功能描述

  • 作为应用的根组件,负责管理全局状态并协调子组件之间的交互。

代码实现

import { useState, useEffect } from 'react';
import TodoInput from './components/TodoInput.jsx';
import TodoList from './components/TodoList.jsx';
import TodoStats from './components/TodoStats.jsx';
import './styles/app.styl';

function App() {
    // 初始化状态
    const [todos, setTodos] = useState(() => {
        const saved = localStorage.getItem('todos');
        return saved ? JSON.parse(saved) : [];
    });

    // 同步到本地存储
    useEffect(() => {
        localStorage.setItem('todos', JSON.stringify(todos));
    }, [todos]);

    // 添加待办事项
    const addTodos = (text) => {
        if (!text?.trim()) return;
        setTodos(prev => [...prev, {
            id: Date.now(),
            text: text.trim(),
            completed: false
        }]);
    };

    // 删除待办事项
    const deleteTodo = (id) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };

    // 切换待办事项状态
    const toggleTodo = (id) => {
        setTodos(todos.map(todo => todo.id === id ? {
            ...todo,
            completed: !todo.completed,
        } : todo));
    };

    // 清除已完成的待办事项
    const clearCompleted = () => {
        setTodos(todos.filter(todo => !todo.completed));
    };

    // 计算活跃和已完成数量
    const activeCount = todos.filter(todo => !todo.completed).length;
    const completedCount = todos.filter(todo => todo.completed).length;

    return (
        <div className="todo-app">
            <h1>My Todo List</h1>
            <TodoInput onAdd={addTodos} />
            <TodoList todos={todos} onDelete={deleteTodo} onToggle={toggleTodo} />
            <TodoStats
                total={todos.length}
                active={activeCount}
                completed={completedCount}
                onClearCompleted={clearCompleted}
            />
        </div>
    );
}

export default App;

关键点解析

  • useState: 用于管理待办事项列表的状态。
  • useEffect: 在每次 todos 发生变化时,同步到本地存储。
  • 事件处理函数addTodosdeleteTodotoggleTodoclearCompleted 分别处理不同的操作。

四、父子组件通信

4.1 数据传递

  • 父组件 App 将数据 (todos) 和操作方法 (onAddonDeleteonToggleonClearCompleted) 通过 props 传递给子组件。
  • 子组件通过回调函数的方式通知父组件进行状态更新。

4.2 示例

  • TodoInput 通过 onAdd 回调将新任务传递给父组件。
  • TodoList 通过 onDelete 和 onToggle 回调分别通知父组件删除和切换任务状态。
  • TodoStats 通过 onClearCompleted 回调通知父组件清除已完成的任务。

五、性能优化与最佳实践

5.1 使用 localStorage

  • 通过 useEffect 钩子,在每次 todos 发生变化时自动保存到本地存储,确保页面刷新后数据不会丢失。

5.2 条件渲染

  • 在 TodoStats 中,只有当存在已完成任务时才渲染“Clear Completed”按钮,避免不必要的 DOM 渲染。

六、总结

通过这个简单的 Todo 应用,我们深入探讨了 React 的核心概念,包括:

  • 组件化开发:将复杂的应用拆分为多个独立的组件,便于维护和扩展。
  • 状态管理:使用 useState 和 useEffect 管理组件内部状态,并与其他组件共享。
  • 父子组件通信:通过 props 传递数据和回调函数,实现组件间的双向通信。

希望这篇文章能帮助你更好地理解 React 的工作原理,并为你未来的项目提供有价值的参考。如果你有任何问题或建议,请随时留言讨论!