简介这是一份面向 React 初学者与前端进阶者的待办事项清单实战项目源码基于 React.js 构建配合 Node.js、Webpack 与 SCSS 完成工程化与样式组织适合用来练习组件拆分、状态管理与列表增删交互。压缩包共 15 个文件约 280KB其中 7 个 jsx 承载主要组件逻辑2 个 js 与 2 个 json 负责入口配置和依赖声明另有 2 个 html 页面骨架、1 个 scss 样式文件与 1 个 md 说明文档结构紧凑、层次分明。已有 129 人学习关注。项目围绕目标内容的添加与删除展开读者可借此理解受控表单、列表渲染、事件绑定与组件通信等核心知识点并参考其目录组织方式快速搭建自己的 React 练习工程是入门到上手阶段较为实用的参考案例。1. 从一份“有待办事项的待办事项清单”说起它到底能解决什么你有没有过这种体验打开某个待办 App想加一条“买牛奶”结果先弹出来一个引导页再让你选分类、设优先级、绑定日历最后你忘了自己要记什么。这份名为react-todo-list的资源恰恰是反着来的——它本身就是一个待办清单但它的“待办事项”里第一条往往是“把待办清单本身跑起来”。听起来像绕口令实际用起来你会发现它解决的是前端从业者最朴素的一个诉求用最短路径拿到一个能跑、能改、能拆的 React 状态管理样本。它适合三类人刚学完 React 基础语法、想找一个完整小项目练手的新手需要给团队新人准备练手素材的带教者以及想快速验证某个状态管理思路、不想从零搭脚手架的熟手。整个资源围绕 JavaScript 生态展开核心是组件拆分、状态提升、列表渲染与本地持久化这几件事。你不需要先理解 Redux 或 Zustand它大概率就是用useState和useEffect撑起来的但恰恰因为简单你才能看清数据从输入框到列表项再到本地存储的完整链路。下面我按“先跑通、再拆解、后避坑”的顺序把这份资源里里外外过一遍。2. 把项目跑起来环境、依赖与首次渲染的完整链路2.1 先确认你手里的 Node 和包管理器版本拿到任何前端资源我第一件事不是看代码而是看package.json里的engines字段和锁文件。这份资源没有附带说明文档但根据常见 React 项目的配置习惯Node 版本低于 16 会在安装依赖时遇到ERR_OSSL_EVP_UNSUPPORTED这类玄学报错。我一般会先跑两条命令确认环境node -v npm -v如果 Node 是 18 或 20基本可以直接往下走如果是 14 或更早建议用 nvm 切一个 LTS 版本。包管理器方面资源里如果存在package-lock.json就用 npm存在pnpm-lock.yaml就用 pnpm不要混用——混用导致的依赖树错乱是新手翻车的高频场景。确认完版本后进入项目根目录执行安装npm install # 或者 pnpm install这一步会拉取 react、react-dom、react-scripts如果是 CRA 脚手架或 vite 相关依赖。安装完成后先别急着npm start打开package.json看一眼scripts字段确认启动命令到底是start、dev还是serve。不同脚手架的习惯不一样CRA 用startVite 用devNext.js 也是dev。跑错命令会提示 “Missing script”这不是项目坏了只是你喊错了口令。2.2 启动开发服务器并验证首次渲染假设启动命令是npm start执行后终端会输出本地地址通常是http://localhost:3000或http://localhost:5173。浏览器打开后你应该看到一个输入框、一个添加按钮以及下方可能为空的列表区域。如果页面白屏按 F12 打开控制台看有没有红色报错。常见的有两类一是Module not found说明依赖没装全回去补npm install二是React is not defined说明 JSX 转换配置有问题检查jsconfig.json或babel.config.js是否存在且配置正确。首次渲染成功后试着输入一条待办比如“写周报”点击添加。如果列表项正常出现说明核心数据流是通的。此时打开浏览器开发者工具的 Application 面板看 Local Storage 里有没有多出一个键值对。这份资源大概率做了本地持久化键名可能是todos或react-todo-list。如果有刷新页面待办还在说明持久化逻辑生效了。如果没有刷新后列表清空那它就是一个纯内存版本你需要自己补持久化——这反而是个很好的练手点后面章节会讲怎么加。3. 拆开看结构组件拆分、状态提升与本地存储的实现细节3.1 组件树长什么样状态放在哪一层一个典型的 React 待办清单组件树不会太深但状态放置的位置决定了你后续改起来顺不顺手。常见做法是顶层App组件持有todos数组和inputValue字符串通过 props 把todos传给TodoList把inputValue和修改函数传给TodoInput。这种“状态提升到最近公共祖先”的模式是 React 官方文档里反复强调的但新手容易犯的错是把inputValue放在TodoInput内部结果App拿不到输入值添加逻辑就断了。你可以打开src目录找到App.js或App.jsx看useState的调用位置。如果useState出现在TodoInput里而添加函数又在App里那大概率是通过回调把值传上去的这种写法也能跑但状态分散在两处调试时要在两个组件之间跳。我一般会建议把inputValue也提升到App让TodoInput变成一个受控组件只负责展示和触发回调。这样整个应用的状态源只有一个排查问题时只需要看App一个文件。3.2 增删改查四个操作的代码落点待办清单的核心操作就四个添加、删除、切换完成状态、编辑文本。这份资源如果只实现了前三个那编辑功能就是留给你的扩展点。先看添加和删除的典型实现// App.js 中维护 todos 状态 const [todos, setTodos] useState([]); // 添加用扩展运算符生成新数组不要直接 push const addTodo (text) { if (!text.trim()) return; // 空输入直接返回避免产生空待办 const newTodo { id: Date.now(), // 用时间戳做临时 id够用但高并发下可能重复 text: text.trim(), completed: false, }; setTodos((prev) [...prev, newTodo]); }; // 删除用 filter 生成新数组 const deleteTodo (id) { setTodos((prev) prev.filter((todo) todo.id ! id)); }; // 切换完成状态用 map 生成新数组 const toggleTodo (id) { setTodos((prev) prev.map((todo) todo.id id ? { ...todo, completed: !todo.completed } : todo ) ); };这三段代码的逻辑说明React 的状态更新必须是不可变的直接todos.push()不会触发重新渲染这是新手最常踩的坑。setTodos传入函数形式(prev) ...是为了避免闭包捕获旧的todos值在快速连续操作时更安全。id用Date.now()在单用户场景下没问题但如果同一毫秒内添加两条id 会重复导致删除时误删。更稳妥的做法是用crypto.randomUUID()现代浏览器都支持。参数说明text.trim()去掉首尾空格防止用户输入一串空格被当成有效待办。completed字段控制样式通常配合 CSS 类名切换删除线。如果你要加编辑功能思路是给每个 todo 增加一个isEditing标记点击编辑时把text渲染成input失焦或回车时更新text并清除标记。3.3 本地持久化useEffect 的依赖数组怎么写如果资源里已经带了持久化代码大概长这样useEffect(() { const saved localStorage.getItem(todos); if (saved) { setTodos(JSON.parse(saved)); } }, []); // 空依赖只在挂载时执行一次 useEffect(() { localStorage.setItem(todos, JSON.stringify(todos)); }, [todos]); // todos 变化时同步写入第一个useEffect负责初始化读取依赖数组为空确保只在组件挂载时跑一次。第二个useEffect负责写入依赖todos每次待办变化都会更新本地存储。这里有个细节如果初始todos是空数组第二个 effect 会在挂载后立刻把空数组写进去覆盖掉之前保存的数据。所以第一个 effect 的读取必须发生在第二个 effect 的写入之前React 的执行顺序是先跑所有 effect 的清理函数再按顺序跑 effect 本身挂载时两个 effect 都会执行读取在前、写入在后刚好能拿到旧数据。但如果你把两个 effect 的顺序写反了就会出现“刷新后数据丢失”的灵异现象。提示localStorage 只能存字符串所以要用JSON.stringify和JSON.parse转换。如果待办数据量很大超过 5MB 会写入失败控制台报QuotaExceededError。待办清单一般不会遇到但如果你要存图片或附件就得换 IndexedDB。4. 避坑与排查从白屏到数据丢失的五个血泪现场4.1 现象页面白屏控制台报 “Objects are not valid as a React child”原因在 JSX 里直接渲染了一个对象。待办清单里最容易出现的地方是todo本身是个对象你写了{todo}而不是{todo.text}。React 不允许对象作为子元素因为对象转成字符串是[object Object]没有意义。解决检查所有{}里的变量确认渲染的是字符串或数字。如果是日期对象先用toLocaleDateString()转成字符串。如果是嵌套对象用JSON.stringify临时打印出来看结构再决定取哪个字段。4.2 现象点击添加按钮没反应但控制台也不报错原因表单的默认提交行为触发了页面刷新。如果你把添加按钮放在form里且没有阻止默认事件点击按钮会提交表单并刷新页面React 状态全部重置看起来就像“没反应”。解决在表单的onSubmit里调用e.preventDefault()或者把按钮的type显式设为button而不是默认的submit。我一般两个都做双保险。4.3 现象删除一条待办结果删掉了另一条原因key用了数组索引index。当列表顺序变化时React 会复用错误的 DOM 节点导致状态错位。比如你删了第一条原本第二条的索引变成 0React 以为它还是原来那条就把第二条的 DOM 更新成了第一条的内容。解决key必须用稳定且唯一的 id不要用索引。如果数据来自后端用数据库主键如果是前端生成的用crypto.randomUUID()或时间戳加随机数。这份资源如果用了索引做 key建议你手动改掉这是性价比最高的一个修复。4.4 现象刷新后待办全部消失但 localStorage 里明明有数据原因读取时机不对。如果你在useState初始化时直接读 localStorage比如useState(localStorage.getItem(todos))读出来的是字符串不是数组后续map会报错。或者你在组件挂载前就读了此时 localStorage 还没准备好虽然极少见。解决用useState的惰性初始化传一个函数进去useState(() JSON.parse(localStorage.getItem(todos)) || [])。这样只在首次渲染时读一次且做了空值兜底。或者用前面说的useEffect方案挂载后再读。4.5 现象输入中文时每敲一个字母就触发一次添加原因用了onChange直接触发添加逻辑而不是先存到inputValue再在提交时添加。中文输入法在拼写阶段会频繁触发input事件如果onChange里直接调addTodo就会产生一堆半成品待办。解决onChange只更新inputValue添加操作绑定在按钮点击或回车键的onKeyDown上。如果要在输入过程中做实时搜索用防抖函数包一层延迟 300ms 再执行。5. 进阶改造给待办清单加上过滤、编辑与拖拽排序跑通基础版本后这份资源最大的价值在于它是一个干净的画布。我一般会按三个方向改造每个方向都能让你对 React 状态管理的理解加深一层。第一个方向是过滤视图。在App里加一个filter状态取值all、active、completed然后根据filter计算派生列表const filteredTodos todos.filter((todo) { if (filter active) return !todo.completed; if (filter completed) return todo.completed; return true; });注意这里不要用useState存过滤后的列表而是每次渲染时从todos和filter计算出来。这样避免了两个状态不同步的问题——你删了一条待办过滤列表自动更新不需要手动同步。这是 React 里“单一数据源”原则的典型应用。第二个方向是行内编辑。给每个TodoItem加一个isEditing状态双击文本时切换为输入框失焦或回车时提交修改。这里有个坑如果isEditing放在TodoItem内部而todos在App里编辑提交后要调用App传下来的updateTodo函数。状态分散但逻辑清晰是可以接受的。另一种做法是把editingId提升到App只允许同时编辑一条实现更简单看你取舍。第三个方向是拖拽排序。这需要引入第三方库常见做法是用dnd-kit/sortable或react-beautiful-dnd。以dnd-kit为例安装后把TodoList包在DndContext里每个TodoItem用useSortable绑定拖拽结束时用arrayMove重新排列todos数组。参数上要注意activationConstraint设一个小的距离阈值否则点击和拖拽会冲突导致想点完成却触发了拖拽。改造方向新增依赖核心 API容易翻车的点过滤视图无Array.filter用状态存派生数据导致不同步行内编辑无受控 input 失焦事件编辑态与全局状态不同步拖拽排序dnd-kit/coreuseSortable、arrayMove点击与拖拽事件冲突验证改造是否成功我习惯用一套固定动作添加三条待办切换其中一条为完成过滤到 active 视图确认只剩两条再切回 all双击编辑一条文本刷新页面确认所有修改都持久化了。这套动作走完没问题基本就能交付给其他人用了。从那以后我每次拿到一个前端小项目都会先跑通再拆组件树最后按这套固定动作验一遍很少再出现“本地好好的、别人一跑就崩”的尴尬。希望这份拆解能帮到你。本文还有配套的精品资源点击获取