从零构建一个待办事项应用:一次关于组件化与状态管理的深度思考

我们今天要做的,是一个再普通不过的小项目------待办事项(Todo List)应用

它看起来简单:输入任务、添加、勾选完成、删除、清空已完成项。但正是这种"简单",恰恰是理解 React 核心思想的最佳载体。


一、如果把所有代码都写在 App.jsx 中,会怎样?

假设我们不拆分组件,直接在 App.jsx 里写完所有的逻辑:

javascript 复制代码
function App() {
  const [todos, setTodos] = useState([]);
  // 添加、删除、切换......全部在这里处理
  return (
    <div>
      <input onKeyPress={...} />
      <button onClick={...}>Add</button>
      <ul>
        {todos.map(...)}
      </ul>
    </div>
  );
}

这会带来什么问题?

可读性下降,维护成本飙升。

当我们需要修改"输入框样式"时,得翻遍整个文件找 input

当我们要支持"编辑任务"功能时,又要在这堆代码中插入新逻辑;

更别说以后加搜索、分类、拖拽排序了......

所以,第一个问题来了:

我们能不能把这个应用拆成几个独立的组件?每个组件负责什么?


二、组件拆解:职责分离才是王道

经过一番思考,我们可以将这个应用划分为三个清晰的部分:

  1. TodoInput ------ 负责用户输入和添加新任务;
  2. TodoList ------ 负责展示所有任务,并提供勾选和删除操作;
  3. TodoStats ------ 展示统计信息(总数、未完成数),并提供"清空已完成"的按钮。

这样做的好处显而易见:

  • 每个组件只关心自己的 UI 和行为;
  • 修改一处不会影响其他部分;
  • 未来复用也更容易(比如另一个项目也需要一个输入框)。

那么下一个问题就来了:

这些组件都需要访问同一个数据 todos,那谁来管理这个数据呢?


三、状态归属:谁该拥有 todos 的控制权?

如果我们让 TodoInput 自己维护 todos,那 TodoList 就无法知道当前有哪些任务了;

如果 TodoList 管理,那 TodoInput 又没法添加新任务。

于是我们得出结论:

todos 必须由最顶层的 App 组件统一管理。

因为它是唯一能同时向所有子组件传递数据和回调函数的地方。

这其实就是 React 的核心设计哲学之一:单向数据流 + 状态提升(State Hoisting)

App 是"数据源",通过 props 把数据传给子组件,子组件通过回调通知父组件更新。


四、深入分析:TodoInput 组件为什么封装 onAdd

我们来看 TodoInput 的实现:

ini 复制代码
const TodoInput = (props) => {
  const { onAdd } = props;
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onAdd(inputValue);
    setInputValue('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
      <button type="submit">Add</button>
    </form>
  );
};

这里有个关键点:我们没有直接在按钮上绑定 onAdd,而是先封装了一个 handleSubmit 函数。

这是为什么?

如果我们直接写 <button onClick={() => onAdd(inputValue)}>Add</button> 呢?

听起来也没错,但有两个隐患:

  1. 无法阻止默认提交行为 :表单提交会导致页面刷新(除非手动调用 preventDefault());
  2. 无法统一处理后续逻辑:比如提交后清空输入框,必须重复写两次(按钮和回车)。

而使用 <form onSubmit> 则天然解决了这两个问题:

  • 浏览器原生支持"回车提交";
  • 所有提交逻辑集中在一个函数中,避免重复。

所以,handleSubmit 不只是"包装一下",而是为了统一处理提交流程,确保用户体验一致、代码简洁可靠。


五、TodoList:如何高效地渲染和交互?

ini 复制代码
const TodoList = ({ todos, onDelete, onToggle }) => {
  return (
    <ul>
      {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)}>Delete</button>
          </li>
        ))
      )}
    </ul>
  );
};

这里有几个值得思考的设计:

  • key={todo.id} :React 需要稳定键值来识别列表项,防止重新渲染时出现闪烁;
  • checked={todo.completed} :使用受控组件,保证 UI 与状态同步;
  • onChange 触发 onToggle :不是直接改 completed,而是通知父组件更新状态,保持单一数据源。

有人可能会问:"为什么不直接在 input 上写 checked={todo.completed} 并监听 onChange?"

答案是:可以!但必须配合 useStatesetTodos 来更新状态,否则就是"不受控组件",违背 React 设计原则。


六、TodoStats:极简但不可忽视的统计层

javascript 复制代码
const TodoStats = ({ total, active, completed, onClearCompleted }) => {
  return (
    <div className="todo-stats">
      <p>Total: {total} | Active: {active} | Completed: {completed}</p>
      {completed > 0 && (
        <button onClick={onClearCompleted} className="clear-btn">
          Clear Completed
        </button>
      )}
    </div>
  );
};

这个组件看似简单,但它承担着两个重要角色:

  1. 信息聚合:将原始数据转化为有意义的统计;
  2. 行为触发:提供"清空已完成"的入口。

它的存在告诉我们:即使是最小的功能模块,也应该被抽象为独立组件,才能更好地组织代码结构。


七、终极难题:页面刷新后数据全丢?怎么办?

现在我们面临一个现实问题:

每次刷新页面,todos 都变成空数组了!

这是因为在浏览器中,JavaScript 的内存是临时的,一旦关闭页面或刷新,状态就会丢失。

那有没有办法让数据持久化?

当然有------localStorage

但我们不想在每次 addTododeleteTodotoggleTodo 时都手动调用 localStorage.setItem(),那样太繁琐。

那有没有更优雅的方式?

答案是:结合 useEffect 使用!

javascript 复制代码
useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);

这段代码的意思是:

"只要 todos 发生变化,就自动保存到 localStorage。"

这带来了巨大的便利:

  • 不需要在每一个方法里重复写存储逻辑;
  • 数据变更即保存,几乎无感知;
  • 符合"副作用"处理的最佳实践。

而且我们还可以在初始化时从 localStorage 加载数据:

ini 复制代码
const [todos, setTodos] = useState(() => {
  const saved = localStorage.getItem('todos');
  return saved ? JSON.parse(saved) : [];
});

这样一来,打开页面时就能恢复上次的状态,用户体验大大提升。


八、总结:这不是一个简单的 Todo 应用

表面上看,这是一个入门级的练习项目。但实际上,它涵盖了 React 开发中的多个核心思想:

概念 在本项目中的体现
组件化 拆分为 Input/List/Stats 三个独立组件
状态管理 todosApp 统一管理,通过 props 传递
受控组件 输入框用 valueonChange 控制,确保数据一致性
表单提交优化 使用 form onSubmit 实现统一提交逻辑
状态持久化 结合 localStorageuseEffect 实现数据保存
不可变性 使用 mapfilter 创建新数组,而非修改原数组

相关推荐
WooaiJava36 分钟前
AI 智能助手项目面试技术要点总结(前端部分)
javascript·大模型·html5
LYFlied40 分钟前
从 Vue 到 React,再到 React Native:资深前端开发者的平滑过渡指南
vue.js·react native·react.js
爱喝白开水a1 小时前
前端AI自动化测试:brower-use调研让大模型帮你做网页交互与测试
前端·人工智能·大模型·prompt·交互·agent·rag
Never_Satisfied1 小时前
在JavaScript / HTML中,关于querySelectorAll方法
开发语言·javascript·html
董世昌411 小时前
深度解析ES6 Set与Map:相同点、核心差异及实战选型
前端·javascript·es6
WeiXiao_Hyy2 小时前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡2 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone2 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09012 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农3 小时前
Vue 2.3
前端·javascript·vue.js