实战演练(一):从零构建一个功能完备的Todo List应用

发布于:2025-08-29 ⋅ 阅读:(15) ⋅ 点赞:(0)

实战演练(一):从零构建一个功能完备的Todo List应用

作者:码力无边

各位React探险家,欢迎集结!我是你们的向导码力无边,这里是《React奇妙之旅》的第六站,也是我们基础阶段的“毕业大戏”!

在过去的五篇文章中,我们一起披荆斩棘,逐一攻克了React的五大基础基石:组件、JSX、Props、State 和 事件处理。我们就像一位屠龙的勇士,收集了五件强大的神器。但是,真正的勇士不仅要会收集神器,更要懂得如何组合它们,发挥出毁天灭地的力量。

今天,我们将放下理论的卷轴,拿起实践的锤子,将所有学到的知识融会贯通,从一个空白的文件开始,亲手锻造出一个每个前端开发者都绕不开的经典项目——Todo List(待办事项)应用

这不仅仅是一次编码练习,更是对你学习成果的一次全面检阅。你将扮演一个“交响乐指挥家”的角色,协调各个组件,管理应用的状态,响应用户的交互。准备好了吗?让我们开始这场精彩的实战演出!

第一章:蓝图规划 —— 运筹帷幄,决胜千里

在敲下第一行代码之前,一位优秀的工程师会先花五分钟时间进行规划。一个好的顶层设计,能让后续的开发事半功倍。

我们的目标功能:

  1. 显示一个待办事项列表。
  2. 能够添加新的待办事项。
  3. 能够切换某个待办事项的完成状态(已完成/未完成)。
  4. 能够删除一个待办事项。

组件拆分 (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. 创建 TodoItemTodoList 组件
这两个是纯展示组件,它们只负责根据传入的 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. 连接 AppTodoForm
App需要把addTodo函数通过props传递给TodoForm

src/App.jsxreturn 部分:

//...
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 ...
}

同样,我们使用了mapfilter这两个数组方法,它们都会返回一个新数组,完美契合了React的“不可变性”原则。

2. 将函数层层传递下去
App -> TodoList -> TodoItem

src/App.jsxreturn 部分:

<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中用它来创建受控组件。
  • 事件处理:我们通过onClickonChange来响应用户的操作,并调用从Props接收的函数来更新State,完成了交互的闭环。

这个小小的Todo List应用,麻雀虽小,五脏俱全。它完美地展示了React的核心思想和工作流程。请务必亲手把这个项目敲一遍,甚至尝试给它增加一些新功能,比如:统计未完成事项的数量、添加编辑功能、使用localStorage进行数据持久化等等。

至此,我们React基础入门阶段的旅程就告一段落了。但React的世界远不止于此。在接下来的“核心进阶”阶段,我们将探索更深层次的话题,比如组件的生命周期与副作用(useEffect)、性能优化(memo),以及更高级的组件通信方式(Context API)。

我是码力无边,为你的坚持和成果感到骄傲!好好庆祝一下,然后准备好迎接更精彩的挑战吧!我们下一阶段见!


网站公告

今日签到

点亮在社区的每一天
去签到