【React-5/Lesson81(2025-12-23)】构建一个完整的 React 待办事项应用(Todo App):从零到本地持久化📝

0 阅读6分钟

📝在现代前端开发中,React 以其组件化、声明式和高效更新的特性成为构建用户界面的首选框架之一。本文将带你深入剖析一个功能完整、结构清晰、具备本地存储能力的 React 待办事项应用(Todo App) 的实现细节。我们将逐层拆解其核心组件、状态管理逻辑、父子通信机制、样式处理以及数据持久化策略,并补充大量相关知识,帮助你真正掌握 React 应用的开发范式。


🧩 应用整体架构概览

该 Todo 应用采用典型的 单页应用(SPA) 结构,以 App.jsx 作为根组件,协调三个主要子组件:

  • TodoInput.jsx:负责接收用户输入并添加新任务。
  • TodoList.jsx:渲染任务列表,支持标记完成与删除操作。
  • TodoStats.jsx:展示任务统计信息,并提供“清除已完成”功能。

整个应用的数据流遵循 单向数据流(Unidirectional Data Flow) 原则:所有状态(todos 数组)由父组件 App 集中管理,子组件通过 props 接收数据和回调函数,实现“只读数据 + 上报事件”的通信模式。


🏗️ 核心组件详解

🔹 App.jsx:状态管理中心与生命周期协调者

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

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

  // 添加任务
  function addTodo(text) {
    setTodos([...todos, { id: Date.now(), text, 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;

  // 监听 todos 变化,同步到 localStorage
  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

📌 关键知识点补充:

  • useState 的惰性初始化(Lazy Initialization)

    useState(() => { ... })
    

    这种写法确保初始化函数只在组件首次渲染时执行一次,避免不必要的计算。这对于从 localStorage 读取数据非常有用。

  • 不可变性(Immutability)原则
    所有状态更新都使用展开运算符(...)或数组方法(filter, map)创建新数组,而非直接修改原数组。这是 React 状态更新的核心要求,确保组件能正确响应变化。

  • useEffect 与副作用管理
    useEffect 在每次 todos 变化后自动将数据序列化并存入 localStorage,实现本地持久化。依赖数组 [todos] 确保仅在 todos 改变时触发。

  • 计算属性
    activeCountcompletedCount 是派生状态(Derived State),每次渲染时重新计算,无需单独用 useState 管理,符合 React 最佳实践。


🔹 TodoInput.jsx:受控组件与表单处理

import { useState } from 'react'

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

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

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={inputValue}
        onChange={e => setInputValue(e.target.value.replace(/\s+/g, '').trim())}
      />
      <button type="submit">Add</button>
    </form>
  )
}

export default TodoInput

📌 关键知识点补充:

  • 受控组件(Controlled Component)
    <input> 的值由 React 状态 inputValue 控制,任何用户输入都通过 onChange 事件处理器更新状态,实现 单向数据绑定。这与 Vue 的 v-model 双向绑定不同,但更符合 React 的声明式理念。
  • 表单提交处理
    使用 <form>onSubmit 事件而非按钮的 onClick,可支持回车键提交,并通过 e.preventDefault() 阻止页面刷新。
  • 输入清理逻辑
    e.target.value.replace(/\s+/g, '').trim() 会移除所有空白字符(包括中间空格),这可能是为了强制输入为连续字符串。但在实际 Todo 应用中,通常只需 .trim() 去除首尾空格即可保留中间空格(如 “Buy milk and eggs”)。此处逻辑略显激进,可根据需求调整。

🔹 TodoList.jsx:列表渲染与交互

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)}>💩</button>
          </li>
        ))
      )}
    </ul>
  )
}

export default TodoList

📌 关键知识点补充:

  • 列表渲染与 Key 属性
    key={todo.id} 是 React 识别列表项身份的关键。使用 Date.now() 作为 ID 虽简单,但在高频添加时可能冲突(毫秒级精度不足)。生产环境应使用更可靠的唯一 ID 生成方案(如 uuid 库)。
  • 条件渲染
    使用三元运算符根据 todos.length 决定显示空状态还是任务列表。
  • 样式动态绑定
    className={todo.completed ? 'completed' : ''} 根据任务状态动态添加 CSS 类,配合样式表实现视觉反馈(如划线、透明度降低等)。
  • 事件处理器内联 vs 提前定义
    此处使用内联箭头函数 () => onToggle(todo.id),简洁但可能影响性能(每次渲染创建新函数)。对于大型列表,可考虑使用 useCallback 优化,但本应用规模小,影响可忽略。

🔹 TodoStats.jsx:状态展示与条件渲染

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

export default TodoStats

📌 关键知识点补充:

  • 短路求值(Short-circuit Evaluation)
    { completed > 0 && <button>... 是 React 中常见的条件渲染技巧。当 completed > 0true 时渲染按钮,否则渲染 false(React 会忽略)。
  • Props 解构
    使用解构赋值 const { total, active, ... } = props 提高代码可读性。

🎨 样式与主题:CSS 与 Stylus 的结合

项目同时使用了:

  • index.css:全局样式,设置页面布局、字体、颜色方案及背景图。

    body {
      margin: 0;
      display: flex;
      place-items: center;
      min-width: 320px;
      min-height: 100vh;
      background-image: url('./assets/react-beautiful.svg');
      /* ... */
    }
    
    • 利用 prefers-color-scheme 媒体查询实现深色/浅色主题自动切换
    • place-items: centeralign-itemsjustify-items 的简写,用于 Flex 容器居中。
  • App.styl(未提供内容,但被引入):
    使用 Stylus 预处理器编写组件局部样式。Stylus 语法简洁,支持嵌套、变量、混入等特性,提升 CSS 可维护性。


⚙️ 项目初始化与构建工具链

根据 README.md 描述,项目使用 Vite 初始化:

npm init vite
# 选择 react + javascript 模板
npm i stylus  # 安装 Stylus 预处理器支持

Vite 提供极速的冷启动和热更新(HMR),极大提升开发体验。配合 React 的模块化开发,形成高效的工作流。


🔄 数据持久化:localStorage 的巧妙运用

应用通过以下方式实现数据持久化:

  1. 初始化时读取

    useState(() => {
      const saved = localStorage.getItem('todos')
      return saved ? JSON.parse(saved) : []
    })
    
  2. 状态变更时写入

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

⚠️ 注意:localStorage 只能存储字符串,因此需用 JSON.stringifyJSON.parse 进行序列化/反序列化。

这种方案简单有效,适用于小型应用。对于复杂应用,可考虑 IndexedDB 或状态管理库(如 Redux + redux-persist)。


🧠 设计模式与最佳实践总结

  • 单一数据源(Single Source of Truth) :所有状态集中在 App 组件。
  • 自上而下的数据流:父组件通过 props 向下传递数据和函数。
  • 子组件无状态(Dumb Components)TodoInputTodoListTodoStats 仅负责 UI 渲染和事件上报。
  • 不可变更新:始终返回新对象/数组,而非修改原数据。
  • 副作用隔离:使用 useEffect 处理副作用(如 localStorage 同步)。
  • 语义化 HTML:使用 <form>, <label>, <input type="checkbox"> 提升可访问性。

✅ 结语

这个看似简单的 Todo 应用,实则涵盖了 React 开发的核心概念:组件化、状态管理、事件处理、条件渲染、列表渲染、表单控制、副作用管理、本地存储以及样式处理。通过深入理解每一行代码背后的原理,你不仅能复现此应用,更能举一反三,构建更复杂的 React 项目。🚀

现在,打开你的编辑器,动手实践吧!