实战演练(一):从零构建一个功能完备的Todo List应用
作者:码力无边
各位React探险家,欢迎集结!我是你们的向导码力无边,这里是《React奇妙之旅》的第六站,也是我们基础阶段的“毕业大戏”!
在过去的五篇文章中,我们一起披荆斩棘,逐一攻克了React的五大基础基石:组件、JSX、Props、State 和 事件处理。我们就像一位屠龙的勇士,收集了五件强大的神器。但是,真正的勇士不仅要会收集神器,更要懂得如何组合它们,发挥出毁天灭地的力量。
今天,我们将放下理论的卷轴,拿起实践的锤子,将所有学到的知识融会贯通,从一个空白的文件开始,亲手锻造出一个每个前端开发者都绕不开的经典项目——Todo List(待办事项)应用。
这不仅仅是一次编码练习,更是对你学习成果的一次全面检阅。你将扮演一个“交响乐指挥家”的角色,协调各个组件,管理应用的状态,响应用户的交互。准备好了吗?让我们开始这场精彩的实战演出!
第一章:蓝图规划 —— 运筹帷幄,决胜千里
在敲下第一行代码之前,一位优秀的工程师会先花五分钟时间进行规划。一个好的顶层设计,能让后续的开发事半功倍。
我们的目标功能:
- 显示一个待办事项列表。
- 能够添加新的待办事项。
- 能够切换某个待办事项的完成状态(已完成/未完成)。
- 能够删除一个待办事项。
组件拆分 (Component Breakdown):
根据功能,我们可以将应用拆分成以下几个组件:
App.jsx
: 根组件。它将是我们的“大脑”,负责管理所有的待办事项数据(即我们的核心state
),并包含所有的业务逻辑(添加、切换、删除)。TodoForm.jsx
: 输入表单组件。包含一个输入框和一个“添加”按钮,负责接收用户的输入。TodoList.jsx
: 列表容器组件。负责接收待办事项数组,并遍历渲染出每一项。TodoItem.jsx
: 单个待办事项组件。负责显示一个待办事项的内容,并包含一个复选框(用于切换状态)和一个删除按钮。
数据流设计 (Data Flow):
- 状态提升 (Lifting State Up):所有待办事项的数据 (
todos
数组) 将作为state
存放在它们共同的最近祖先组件——App.jsx
中。 - 数据向下流动:
App
组件将todos
数组通过props
传递给TodoList
。 - 事件向上传递:当用户在子组件中进行操作时(如在
TodoForm
中添加,或在TodoItem
中切换/删除),子组件会调用从App
通过props
传递下来的函数,来通知App
组件更新自己的state
。
蓝图已经清晰,让我们开始动工!
第二章:搭建骨架 —— 状态初始化与静态渲染
首先,我们先把应用的静态部分展示出来。
1. 准备工作
清空你的 src
目录下的 App.jsx
, App.css
, index.css
内容。在 src
下新建 components
文件夹。
2. 初始化App
组件的状态
在 App.jsx
中,我们用 useState
来定义一个包含初始待办事项的数组。
// src/App.jsx
import { useState } from 'react';
import './App.css';
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React 基础', completed: true },
{ id: 2, text: '构建一个 Todo List 应用', completed: false },
{ id: 3, text: '准备下一篇文章', completed: false },
]);
return (
<div className="app">
<h1>我的待办事项</h1>
{/* 后面会在这里添加其他组件 */}
</div>
);
}
export default App;
3. 创建 TodoItem
和 TodoList
组件
这两个是纯展示组件,它们只负责根据传入的 props
来渲染UI。
src/components/TodoItem.jsx
:
// src/components/TodoItem.jsx
import React from 'react';
function TodoItem({ todo }) { // 接收一个todo对象作为prop
return (
<li className="todo-item">
<span>{todo.text}</span>
</li>
);
}
export default TodoItem;
src/components/TodoList.jsx
:
// src/components/TodoList.jsx
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos }) { // 接收todos数组作为prop
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
export default TodoList;
注意这里我们完美地运用了.map()
来渲染列表,并且为每一项都提供了稳定且唯一的key
。
4. 组装到 App
组件中
// src/App.jsx
import { useState } from 'react';
import TodoList from './components/TodoList'; // 导入TodoList
import './App.css';
function App() {
const [todos, setTodos] = useState([/* ... */]);
return (
<div className="app">
<h1>我的待办事项</h1>
<TodoList todos={todos} /> {/* 将state作为prop传递下去 */}
</div>
);
}
export default App;
5. 添加一些基本样式
在 src/App.css
中加入一些CSS让它看起来不那么简陋。
/* src/App.css */
.app {
max-width: 500px;
margin: 50px auto;
padding: 20px;
background-color: #f4f4f4;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
}
.todo-list {
list-style-type: none;
padding: 0;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #fff;
border-bottom: 1px solid #ddd;
}
.todo-item:last-child {
border-bottom: none;
}
现在,刷新浏览器,你应该能看到一个静态的待办事项列表了!我们的骨架已经搭建完毕。
第三章:注入灵魂 —— 添加新事项
接下来,我们要让应用能响应用户的输入。
1. 创建 TodoForm
组件
这个组件需要管理自己的输入框状态。
src/components/TodoForm.jsx
:
// src/components/TodoForm.jsx
import React, { useState } from 'react';
function TodoForm() {
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认提交行为
if (!input.trim()) return; // 不允许添加空白内容
// 这里需要一种方式通知App组件添加新的todo
// ... 待实现 ...
setInput(''); // 添加后清空输入框
};
return (
<form className="todo-form" onSubmit={handleSubmit}>
<input
type="text"
className="todo-input"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="添加新的待办..."
/>
<button type="submit" className="todo-button">添加</button>
</form>
);
}
export default TodoForm;
这是一个完美的“受控组件”实践。
2. 在 App
组件中定义添加逻辑
// src/App.jsx
// ... imports
function App() {
const [todos, setTodos] = useState([/* ... */]);
// 定义添加todo的函数
const addTodo = (text) => {
const newTodo = {
id: Date.now(), // 使用时间戳作为临时唯一id
text: text,
completed: false,
};
setTodos([...todos, newTodo]); // 使用展开语法创建新数组
};
// ... return statement
}
我们使用了函数式更新的最佳实践:通过展开运算符...
创建一个全新的数组,而不是直接修改旧的todos
数组。
3. 连接 App
和 TodoForm
App
需要把addTodo
函数通过props
传递给TodoForm
。
src/App.jsx
的 return
部分:
//...
import TodoForm from './components/TodoForm'; // 别忘了导入
//...
return (
<div className="app">
<h1>我的待办事项</h1>
<TodoForm onAddTodo={addTodo} /> {/* 将函数作为prop传递 */}
<TodoList todos={todos} />
</div>
);
src/components/TodoForm.jsx
中接收并调用 prop:
// ...
// function TodoForm() -> function TodoForm({ onAddTodo })
function TodoForm({ onAddTodo }) {
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
onAddTodo(input); // 调用从父组件传来的函数
setInput('');
};
// ...
}
现在,试试在输入框里输入内容并点击“添加”,新的待办事项神奇地出现了!
第四章:赋予交互 —— 切换与删除
最后一步,让我们给每个TodoItem
添加交互功能。
1. 在 App
组件中定义切换和删除的逻辑
// src/App.jsx
// ...
function App() {
// ... useState 和 addTodo ...
// 切换完成状态
const toggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// 删除todo
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// ... return statement ...
}
同样,我们使用了map
和filter
这两个数组方法,它们都会返回一个新数组,完美契合了React的“不可变性”原则。
2. 将函数层层传递下去
App
-> TodoList
-> TodoItem
src/App.jsx
的 return
部分:
<TodoList
todos={todos}
onToggleTodo={toggleTodo}
onDeleteTodo={deleteTodo}
/>
src/components/TodoList.jsx
:
// function TodoList({ todos }) -> function TodoList({ todos, onToggleTodo, onDeleteTodo })
function TodoList({ todos, onToggleTodo, onDeleteTodo }) {
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggleTodo={onToggleTodo} // 继续向下传递
onDeleteTodo={onDeleteTodo} // 继续向下传递
/>
))}
</ul>
);
}
3. 在 TodoItem
中接收并使用这些函数
src/components/TodoItem.jsx
:
// function TodoItem({ todo }) -> function TodoItem({ todo, onToggleTodo, onDeleteTodo })
function TodoItem({ todo, onToggleTodo, onDeleteTodo }) {
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggleTodo(todo.id)} // 使用箭头函数传递id
/>
<span className="todo-text">{todo.text}</span>
<button className="delete-button" onClick={() => onDeleteTodo(todo.id)}>
删除
</button>
</li>
);
}
4. 完善样式
在 App.css
中添加完成状态和按钮的样式。
/* ... 其他样式 ... */
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-text {
flex-grow: 1;
margin: 0 10px;
}
.delete-button {
background-color: #ff4d4d;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.delete-button:hover {
background-color: #cc0000;
}
/* ... 也可以给表单加点样式 ... */
现在,你的Todo List应用功能已经完全实现了!你可以添加、切换完成状态、删除待办事项了!
总结:一次完美的知识巡礼
恭喜你,成功地完成了你的第一个React实战项目!让我们停下来,回顾一下我们是如何运用“五大神器”的:
- 组件化:我们将应用拆分成了
App
,TodoForm
,TodoList
,TodoItem
四个高内聚、低耦合的组件。 - JSX:我们用声明式、类似HTML的语法清晰地描述了每个组件的UI结构。
- Props:我们通过Props将数据(
todos
数组)和行为(addTodo
等函数)从父组件传递到子组件,搭建了组件间的通信网络。 - State:我们在
App
组件中使用useState
来管理应用的核心数据,并在数据变化时自动驱动UI更新。我们还在TodoForm
中用它来创建受控组件。 - 事件处理:我们通过
onClick
和onChange
来响应用户的操作,并调用从Props接收的函数来更新State,完成了交互的闭环。
这个小小的Todo List应用,麻雀虽小,五脏俱全。它完美地展示了React的核心思想和工作流程。请务必亲手把这个项目敲一遍,甚至尝试给它增加一些新功能,比如:统计未完成事项的数量、添加编辑功能、使用localStorage进行数据持久化等等。
至此,我们React基础入门阶段的旅程就告一段落了。但React的世界远不止于此。在接下来的“核心进阶”阶段,我们将探索更深层次的话题,比如组件的生命周期与副作用(useEffect
)、性能优化(memo
),以及更高级的组件通信方式(Context API
)。
我是码力无边,为你的坚持和成果感到骄傲!好好庆祝一下,然后准备好迎接更精彩的挑战吧!我们下一阶段见!