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

我们今天要做的,是一个再普通不过的小项目------待办事项(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 创建新数组,而非修改原数组

相关推荐
重铸码农荣光2 小时前
🎯 从零搭建一个 React Todo 应用:父子通信、状态管理与本地持久化全解析!
前端·react.js·架构
用户4099322502122 小时前
Vue3 v-if与v-show:销毁还是隐藏,如何抉择?
前端·vue.js·后端
Mr_chiu2 小时前
🚀 效率暴增!Vue.js开发必知的15个神级提效工具
前端
shanLion2 小时前
Vite项目中process报红问题的深层原因与解决方案
前端·javascript
前端小万2 小时前
草稿
前端
闲云一鹤2 小时前
将地图上的 poi 点位导出为 excel,并转换为 shp 文件
前端·cesium
岁月宁静3 小时前
MasterGo AI 实战教程:10分钟生成网页设计图(附案例演示)
前端·aigc·视觉设计
狗头大军之江苏分军3 小时前
快手12·22事故原因的合理猜测
前端·后端
我命由我123453 小时前
CSS 锚点定位 - 锚点定位引入(anchor-name、position-anchor)
开发语言·前端·javascript·css·学习·html·学习方法