React 入门实战:从零搭建 TodoList(父子通信+本地存储+Stylus)
作为 React 入门的经典案例,TodoList 几乎涵盖了 React 基础开发中最核心的知识点------组件拆分、父子组件通信、响应式状态管理、本地存储持久化,再搭配 Stylus 预处理和 Vite 构建,既能夯实基础,又能贴近实际开发场景。
本文将基于完整可运行代码,一步步拆解 React TodoList 的实现逻辑,重点讲解父子组件通信的核心技巧、本地存储的优雅实现,以及组件化开发的最佳实践,适合 React 新手入门学习,也适合作为基础复盘素材。
一、项目环境与技术栈
先明确本次实战的技术栈组合,都是前端开发中高频使用的工具,简单易上手:
- 构建工具:Vite(替代 Webpack,启动更快、打包更高效,适合中小型项目快速开发)
- 核心框架 :React(使用 Hooks 语法,
useState管理组件状态,useEffect处理副作用) - 样式预处理:Stylus(比 CSS 更简洁,支持嵌套、变量、混合等特性,提升样式开发效率)
- 本地存储:localStorage(实现 Todo 数据持久化,刷新页面数据不丢失)
项目初始化命令(快速搭建基础环境):
bash
# 初始化 Vite + React 项目
npm create vite@latest react-todo-demo -- --template react
# 进入项目目录
cd react-todo-demo
# 安装依赖
npm install
# 安装 Stylus(样式预处理)
npm install stylus --save-dev
# 启动项目
npm run dev
二、项目结构与组件拆分
组件化是 React 开发的核心思想,一个清晰的项目结构能提升代码可读性和可维护性。本次 TodoList 我们拆分为 4 个核心组件,遵循「单一职责原则」,每个组件只负责自己的功能:
bash
src/
├── components/ # 自定义组件目录
│ ├── TodoInput.js # 输入框组件:添加新 Todo
│ ├── TodoList.js # 列表组件:展示所有 Todo、切换完成状态、删除 Todo
│ └── TodoStats.js # 统计组件:展示 Todo 总数、活跃数、已完成数,清空已完成
├── styles/ # 样式目录
│ └── app.styl # 全局样式(使用 Stylus 编写)
└── App.js # 根组件:管理全局状态、协调所有子组件
核心逻辑:根组件 App 作为「数据中心」,持有所有 Todo 数据和修改数据的方法,通过 props 将数据和方法传递给子组件;子组件不直接修改数据,只能通过父组件传递的方法提交修改请求,实现数据统一管理。
三、核心功能实现(附完整代码解析)
下面从根组件到子组件,一步步解析每个功能的实现逻辑,重点讲解父子通信、状态管理和本地存储的核心细节。
3.1 根组件 App.js:数据中心与组件协调
App 组件是整个 TodoList 的核心,负责:初始化 Todo 数据、定义修改数据的方法、监听数据变化并持久化到本地存储、传递数据和方法给子组件。
javascript
import { useState, useEffect } from 'react'
import './styles/app.styl'
import TodoList from './components/TodoList'
import TodoInput from './components/TodoInput'
import TodoStats from './components/TodoStats'
function App() {
// 1. 初始化 Todo 数据(本地存储持久化)
// useState 高级用法:传入函数,避免每次渲染都执行 JSON.parse(性能优化)
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos');
// 本地存储有数据则解析,无数据则初始化为空数组
return saved ? JSON.parse(saved) : [];
})
// 2. 定义修改数据的方法(供子组件调用)
// 新增 Todo:接收子组件传递的文本,添加到 todos 数组
const addTodo = (text) => {
// 注意:React 状态不可直接修改,需通过扩展运算符创建新数组
setTodos([...todos, {
id: Date.now(), // 用时间戳作为唯一 ID,简单高效
text, // 子组件传入的 Todo 文本
completed: false, // 初始状态为未完成
}])
}
// 删除 Todo:接收子组件传递的 ID,过滤掉对应 Todo
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
// 切换 Todo 完成状态:接收 ID,修改对应 Todo 的 completed 属性
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
// 清空已完成 Todo:过滤掉所有 completed 为 true 的 Todo
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed))
}
// 3. 计算统计数据(传递给 TodoStats 组件)
const activeCount = todos.filter(todo => !todo.completed).length; // 活跃 Todo 数
const completedCount = todos.filter(todo => todo.completed).length; // 已完成 Todo 数
// 4. 副作用:监听 todos 变化,持久化到本地存储
// 依赖数组 [todos]:只有 todos 变化时,才执行该函数
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos])
// 5. 渲染子组件,通过 props 传递数据和方法
return (
My Todo List
{/* 输入框组件:传递 addTodo 方法,用于新增 Todo */}
<TodoInput onAdd={addTodo}/>
{/* 列表组件:传递 todos 数据,以及删除、切换状态的方法 */}
<TodoList
todos={todos}
onDelete={deleteTodo}
onToggle={toggleTodo}
/>
{/* 统计组件:传递统计数据和清空方法 */}<TodoStats
total={todos.length}
active={activeCount}
completed={completedCount}
onClearCompleted={clearCompleted}
/>
)
}
export default App
关键知识点解析:
- useState 高级用法 :传入函数初始化状态,避免每次组件渲染都执行
JSON.parse,提升性能(尤其数据量大时)。 - 状态不可变 :React 状态是只读的,修改 todos 时,必须通过
filter、map、扩展运算符等方式创建新数组,不能直接修改原数组(如todos.push(...)是错误写法)。 - useEffect 副作用:监听 todos 变化,将数据存入 localStorage,实现「刷新页面数据不丢失」;依赖数组 [todos] 确保只有数据变化时才执行存储操作,避免无效渲染。
- 父子通信基础:父组件通过 props 向子组件传递数据(如 todos)和方法(如 addTodo),子组件通过调用这些方法修改父组件的状态。
3.2 子组件 1:TodoInput.js(输入框组件)
负责接收用户输入的 Todo 文本,通过父组件传递的 onAdd 方法,将文本提交给父组件,实现新增 Todo 功能。
javascript
import { useState } from 'react'
const TodoInput = (props) => {
// 接收父组件传递的 addTodo 方法
const { onAdd } = props;
// 本地状态:管理输入框的值(React 单向绑定)
const [inputValue, setInputValue] = useState('');
// 处理表单提交:新增 Todo
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认提交行为(避免页面刷新)
// 简单校验:输入不能为空
if (!inputValue.trim()) return;
// 调用父组件传递的方法,提交输入的文本
onAdd(inputValue);
// 清空输入框
setInputValue('');
}
return (
<form className="todo-input" onSubmit={<input
type="text"
value={绑定:输入框的值由 inputValue 控制
onChange={e => setInputValue(e.target.value)} // 监听输入变化,更新状态
placeholder="请输入 Todo..."
/>
)
}
export default TodoInput
关键知识点解析:
- React 单向绑定 :React 不支持 Vue 中的 v-model 双向绑定(为了性能优化,避免不必要的视图更新),通过「value + onChange」实现数据与视图的同步------输入框的值由
inputValue控制,输入变化时通过onChange更新inputValue。 - 子父通信 :子组件通过调用父组件传递的
onAdd方法,将输入的文本传递给父组件,实现「子组件向父组件传递数据」(核心:父传方法,子调用方法传参)。 - 表单校验:简单的非空校验,避免添加空 Todo,提升用户体验。
3.3 子组件 2:TodoList.js(列表组件)
负责展示所有 Todo 列表,接收父组件传递的 todos 数据,以及删除、切换完成状态的方法,实现 Todo 列表的渲染、状态切换和删除功能。
ini
const TodoList = (props) => {
// 接收父组件传递的数据和方法
const { todos, onDelete, onToggle } = props;
return (
{
// 空状态处理:没有 Todo 时显示提示
todos.length === 0 ? (
No todos yet!
) : (
// 遍历 todos 数组,渲染每个 Todo 项
todos.map(todo => (
<li
key={唯一 key,React 用于优化渲染(避免重复渲染)
className={todo.completed ? 'completed' : ''} // 根据完成状态添加样式
>{todo.text}
{/* 删除按钮:点击时调用 onDelete 方法,传递当前 Todo 的 ID */}<button onClick={ onDelete(todo.id)}>X
))
)
}
)
}
export default TodoList
关键知识点解析:
- 列表渲染 :使用
map遍历 todos 数组,渲染每个 Todo 项;必须添加key属性(推荐用唯一 ID),React 通过 key 识别列表项的变化,优化渲染性能。 - 条件渲染:判断 todos 数组长度,为空时显示「No todos yet!」,提升空状态体验。
- 状态切换与删除 :复选框的
checked属性绑定todo.completed,点击时调用onToggle方法传递 Todo ID;删除按钮点击时调用onDelete方法传递 ID,实现子组件触发父组件数据修改。
3.4 子组件 3:TodoStats.js(统计组件)
负责展示 Todo 统计信息(总数、活跃数、已完成数),以及清空已完成 Todo 的功能,接收父组件传递的统计数据和清空方法。
javascript
const TodoStats = (props) => {
// 接收父组件传递的统计数据和清空方法
const { total, active, completed, onClearCompleted } = props;
return (
{/* 展示统计信息 */}
Total: {total} | Active: {active} | Completed: {completed} {
// 条件渲染:只有已完成数 > 0 时,显示清空按钮
completed > 0 && (
<button
onClick={ className="clear-btn"
>Clear Completed
)
}
)
}
export default TodoStats
关键知识点解析:
- 条件渲染优化:只有当已完成 Todo 数大于 0 时,才显示「Clear Completed」按钮,避免按钮无效显示,提升用户体验。
- 父子通信复用:和其他子组件一样,通过 props 接收父组件的方法(onClearCompleted),点击按钮时调用,触发父组件清空已完成 Todo 的操作。
3.5 样式文件 app.styl(Stylus 编写)
使用 Stylus 编写全局样式,利用嵌套、变量等特性,简化样式编写,提升可维护性(示例代码):
yaml
// 定义变量(可复用)
$primary-color = #42b983
$gray-color = #f5f5f5
$completed-color = #999
.todo-app
max-width: 600px
margin: 2rem auto
padding: 0 1rem
font-family: 'Arial', sans-serif
.todo-input
display: flex
gap: 0.5rem
margin-bottom: 1.5rem
input
flex: 1
padding: 0.5rem
border: 1px solid #ddd
border-radius: 4px
button
padding: 0.5rem 1rem
background: $primary-color
color: white
border: none
border-radius: 4px
cursor: pointer
.todo-list
list-style: none
padding: 0
margin: 0 0 1.5rem 0
li
display: flex
justify-content: space-between
align-items: center
padding: 0.8rem
margin-bottom: 0.5rem
background: white
border-radius: 4px
box-shadow: 0 2px 4px rgba(0,0,0,0.1)
&.completed
span
text-decoration: line-through
color: $completed-color
label
display: flex
align-items: center
gap: 0.5rem
button
background: #ff4444
color: white
border: none
border-radius: 50%
width: 20px
height: 20px
display: flex
align-items: center
justify-content: center
cursor: pointer
.empty
text-align: center
padding: 1rem
color: $gray-color
font-style: italic
.todo-stats
display: flex
justify-content: space-between
align-items: center
padding: 0.8rem
background: $gray-color
border-radius: 4px
.clear-btn
padding: 0.3rem 0.8rem
background: #ff4444
color: white
border: none
border-radius: 4px
cursor: pointer
四、核心知识点总结(重点!)
通过这个 TodoList 案例,我们掌握了 React 基础开发的核心技能,尤其是父子组件通信和状态管理,这也是 React 开发中最常用的知识点,总结如下:
4.1 父子组件通信(核心)
React 中组件通信的核心是「单向数据流」,即数据从父组件流向子组件,子组件通过调用父组件传递的方法修改数据,具体分为两种情况:
- 父传子 :通过 props 传递数据(如 todos、total、active)和方法(如 addTodo、onDelete),子组件通过
props.xxx接收使用。 - 子传父:父组件传递一个方法给子组件,子组件调用该方法时传递参数,父组件通过方法参数接收子组件的数据(如 TodoInput 传递输入文本给 App)。
4.2 兄弟组件通信(间接实现)
React 中没有直接的兄弟组件通信方式,需通过「父组件作为中间媒介」实现:
例如 TodoInput(新增 Todo)和 TodoList(展示 Todo)是兄弟组件,它们的通信流程是:TodoInput → 调用父组件 addTodo 方法传递文本 → 父组件更新 todos 状态 → 父组件通过 props 将更新后的 todos 传递给 TodoList → TodoList 重新渲染。
4.3 状态管理与本地存储
- 使用
useState管理组件状态,遵循「状态不可变」原则,修改状态必须通过setXXX方法,且不能直接修改原状态。 - 使用
useEffect处理副作用(如本地存储),依赖数组控制副作用的执行时机,避免无效渲染。 - localStorage 持久化:将 todos 数据存入本地存储,页面刷新时从本地存储读取数据,实现数据不丢失(注意:localStorage 只能存储字符串,需用
JSON.stringify和JSON.parse转换)。
4.4 组件化开发最佳实践
- 单一职责原则:每个组件只负责一个功能(如 TodoInput 只负责输入,TodoList 只负责展示)。
- 复用性:组件设计时尽量通用,避免硬编码(如 TodoList 不关心 Todo 的具体内容,只负责渲染和触发方法)。
- 用户体验:添加空状态、表单校验、条件渲染等细节,提升用户使用体验。
五、最终效果与扩展方向
5.1 最终效果
- 输入文本,点击 Add 按钮新增 Todo。
- 点击复选框,切换 Todo 完成状态(已完成显示删除线)。
- 点击 Todo 项右侧的 X,删除对应的 Todo。
- 底部显示 Todo 统计信息,已完成数大于 0 时显示清空按钮。
- 刷新页面,所有 Todo 数据不丢失(本地存储生效)。
5.2 扩展方向(进阶练习)
如果想进一步提升,可以尝试以下扩展功能,巩固 React 基础:
- 添加 Todo 编辑功能(双击 Todo 文本可编辑)。
- 添加筛选功能(全部、活跃、已完成)。
- 使用
useReducer替代useState管理复杂状态(适合 todos 操作较多的场景)。 - 添加动画效果(如 Todo 新增/删除时的过渡动画)。
- 使用 Context API 实现跨组件状态共享(替代 props 层层传递)。
六、总结
TodoList 虽然是 React 入门案例,但涵盖了 React 开发中最核心的知识点------组件拆分、父子通信、状态管理、副作用处理、本地存储,以及 Stylus 预处理和 Vite 构建的使用。
对于 React 新手来说,建议亲手敲一遍完整代码,重点理解「单向数据流」和「父子组件通信」的逻辑,再尝试扩展功能,逐步夯实 React 基础。
✨ 附:项目运行命令
bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 打包构建(部署用)
npm run build