📝在现代前端开发中,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改变时触发。 -
计算属性:
activeCount和completedCount是派生状态(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 > 0为true时渲染按钮,否则渲染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: center是align-items和justify-items的简写,用于 Flex 容器居中。
- 利用
-
App.styl(未提供内容,但被引入):
使用 Stylus 预处理器编写组件局部样式。Stylus 语法简洁,支持嵌套、变量、混入等特性,提升 CSS 可维护性。
⚙️ 项目初始化与构建工具链
根据 README.md 描述,项目使用 Vite 初始化:
npm init vite
# 选择 react + javascript 模板
npm i stylus # 安装 Stylus 预处理器支持
Vite 提供极速的冷启动和热更新(HMR),极大提升开发体验。配合 React 的模块化开发,形成高效的工作流。
🔄 数据持久化:localStorage 的巧妙运用
应用通过以下方式实现数据持久化:
-
初始化时读取:
useState(() => { const saved = localStorage.getItem('todos') return saved ? JSON.parse(saved) : [] }) -
状态变更时写入:
useEffect(() => { localStorage.setItem('todos', JSON.stringify(todos)) }, [todos])
⚠️ 注意:
localStorage只能存储字符串,因此需用JSON.stringify和JSON.parse进行序列化/反序列化。
这种方案简单有效,适用于小型应用。对于复杂应用,可考虑 IndexedDB 或状态管理库(如 Redux + redux-persist)。
🧠 设计模式与最佳实践总结
- 单一数据源(Single Source of Truth) :所有状态集中在
App组件。 - 自上而下的数据流:父组件通过
props向下传递数据和函数。 - 子组件无状态(Dumb Components) :
TodoInput、TodoList、TodoStats仅负责 UI 渲染和事件上报。 - 不可变更新:始终返回新对象/数组,而非修改原数据。
- 副作用隔离:使用
useEffect处理副作用(如 localStorage 同步)。 - 语义化 HTML:使用
<form>,<label>,<input type="checkbox">提升可访问性。
✅ 结语
这个看似简单的 Todo 应用,实则涵盖了 React 开发的核心概念:组件化、状态管理、事件处理、条件渲染、列表渲染、表单控制、副作用管理、本地存储以及样式处理。通过深入理解每一行代码背后的原理,你不仅能复现此应用,更能举一反三,构建更复杂的 React 项目。🚀
现在,打开你的编辑器,动手实践吧!