1. 项目概述为什么是 Todo List 与 React 19如果你正在学习 React或者想检验一下自己 React 的水平Todo List 这个项目几乎是绕不开的“新手村”任务。它看似简单一个输入框一个列表几个操作按钮但麻雀虽小五脏俱全。它几乎涵盖了现代前端应用开发的所有核心概念状态管理、用户交互、列表渲染、数据增删改查。而当我们用 React 19 来重新审视这个经典项目时你会发现它不再仅仅是一个练习而是一个绝佳的、用来“吃透”React 19 新特性与组件化设计思想的实战沙盒。React 19 带来了一系列旨在提升开发者体验和性能的改进比如更智能的编译器优化、新的 Actions API、以及一些内置 Hook 的增强。但万变不离其宗React 的核心依然是组件化和数据驱动。Todo List 项目之所以经典正是因为它能将这些抽象概念具象化。你每添加一个待办事项都是在操作状态每完成一项都是在触发状态更新与 UI 重渲染而如何让新增的 Todo 项组件、列表组件、筛选组件之间高效地“对话”这就是组件通信的艺术。所以这个项目的目的很明确我们不满足于实现一个能跑的 Todo List。我们要从零开始搭建一个结构清晰、可维护性高的 React 19 应用并在这个过程中把父子通信、兄弟通信、跨层级通信、以及状态管理库等通信模式掰开揉碎了讲清楚。无论你是刚接触 React 的新手还是想了解 React 19 新特性的老手跟着走完这一趟你收获的将不仅仅是一个 Todo 应用更是一套应对复杂前端应用的组件设计与通信方法论。2. 项目整体架构与设计思路在动手写代码之前花点时间思考架构是值得的。一个糟糕的架构会让后续的组件通信变得异常复杂和混乱。我们的目标是构建一个职责分明、数据流清晰的应用。2.1 核心组件拆解与职责划分首先我们把整个应用拆分成几个核心的、功能单一的组件。这符合 React 的“单一职责原则”。App组件应用的根组件它不负责具体的 UI 渲染而是作为状态管理中心和数据流的中枢。它持有最顶层的状态如所有的 Todo 项并定义修改这些状态的方法。然后通过 Props 将这些状态和方法“注入”到子组件中。TodoInput组件一个受控的输入框组件。它的职责非常纯粹接收用户的输入并在用户点击“添加”按钮或按回车键时将新的待办事项文本提交给父组件App。它自身不管理 Todo 列表。TodoList组件负责渲染 Todo 项列表。它接收来自App的todos数组和过滤状态根据条件筛选后将每个 Todo 项的数据传递给子组件TodoItem进行渲染。它像一个列表的“容器”或“视图”。TodoItem组件代表单个待办事项的展示单元。它接收一个todo对象作为 Prop展示其内容、完成状态并提供“完成/取消”、“编辑”、“删除”等操作的 UI。当用户触发这些操作时它需要调用父组件通过 Props 传递下来的方法来更新数据。TodoFilter组件提供“全部”、“进行中”、“已完成”等筛选按钮。点击不同按钮时它需要通知App组件当前选中的过滤条件从而驱动TodoList重新渲染过滤后的列表。这个架构的核心思想是状态上提。所有可变的数据todosfilter都存放在最接近的共同祖先组件——App中。子组件大多是“纯”的展示组件或简单的交互组件它们通过 Props 接收数据和回调函数。当子组件需要修改数据时调用这些回调函数由App来执行实际的 state 更新。这样数据流是单向且可预测的从App流向子组件子组件的动作通过回调函数“冒泡”回App。2.2 技术选型与 React 19 新特性考量对于这个项目我们坚持使用最核心的 React 技术栈以聚焦于组件通信本身。构建工具使用Vite。它启动快、配置简单对 React 的支持开箱即用能让我们快速进入开发。命令npm create vitelatest my-todo-app -- --template react即可搭建。状态管理暂不使用 Redux、MobX 等第三方库。我们的首要目标是熟练掌握 React 内置的useState、useReducer以及 Context 来完成所有通信。在项目后期我们会讨论何时需要考虑引入这些库。样式方案为了简洁我们使用普通的 CSS Modules 或简单的内联样式。这不会分散我们对逻辑的注意力。React 19 新特性实践useHook这是一个实验性但非常有前景的 Hook用于更优雅地处理异步数据如从模拟 API 读取初始 Todos。我们可以在数据加载部分尝试使用它但需要明确它尚未稳定。Actions APIReact 19 为表单处理引入了更强大的 Actions 概念与useFormStatus、useFormState等 Hook 配合。我们的TodoInput本质上是一个表单可以尝试用新的 Actions 模式重构体验声明式数据提交。编译器优化我们不需要手动做任何事情但要知道 React 19 的编译器如 React Forget旨在自动生成等效于useMemo和useCallback的代码这可能会影响我们性能优化的习惯。在编写代码时我们仍会按需使用这些 Hook 来讲解概念但心里要明白未来的趋势。注意React 19 的某些新特性可能仍处于 Canary 或实验性频道。为了稳定性我们的核心代码将基于稳定版的模式编写但会开辟独立章节来探讨如何用新特性进行重构和优化。3. 基础搭建与核心状态管理让我们开始动手。首先初始化项目并搭建起最基础的状态管理结构。3.1 项目初始化与组件骨架使用 Vite 快速创建项目npm create vitelatest react19-todo -- --template react cd react19-todo npm install npm run dev清理src/App.jsx和src/index.css然后创建我们的组件文件结构src/ ├── components/ │ ├── TodoInput.jsx │ ├── TodoList.jsx │ ├── TodoItem.jsx │ └── TodoFilter.jsx ├── App.jsx ├── main.jsx └── index.css首先在App.jsx中定义我们的核心状态和组件骨架。// src/App.jsx import { useState } from react; import ./App.css; import TodoInput from ./components/TodoInput; import TodoList from ./components/TodoList; import TodoFilter from ./components/TodoFilter; // 定义单个 Todo 项的数据结构 export const FILTER_STATUS { ALL: all, ACTIVE: active, COMPLETED: completed }; function App() { // 状态1: Todo列表 const [todos, setTodos] useState([ { id: 1, text: 学习 React 19 新特性, completed: false }, { id: 2, text: 搭建 TodoList 项目, completed: true }, { id: 3, text: 深入理解组件通信, completed: false }, ]); // 状态2: 当前筛选条件 const [filter, setFilter] useState(FILTER_STATUS.ALL); // 计算属性根据筛选条件得到要展示的todos const getFilteredTodos () { switch(filter) { case FILTER_STATUS.ACTIVE: return todos.filter(todo !todo.completed); case FILTER_STATUS.COMPLETED: return todos.filter(todo todo.completed); default: return todos; } }; // 操作方法1: 添加Todo const addTodo (text) { const newTodo { id: Date.now(), // 简单用时间戳作为id text, completed: false }; setTodos([...todos, newTodo]); }; // 操作方法2: 切换Todo完成状态 const toggleTodo (id) { setTodos(todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo )); }; // 操作方法3: 删除Todo const deleteTodo (id) { setTodos(todos.filter(todo todo.id ! id)); }; // 操作方法4: 更新Todo文本 const updateTodoText (id, newText) { setTodos(todos.map(todo todo.id id ? { ...todo, text: newText } : todo )); }; return ( div classNameapp h1⚛️ React 19 Todo List/h1 TodoInput onAddTodo{addTodo} / TodoFilter currentFilter{filter} onFilterChange{setFilter} / TodoList todos{getFilteredTodos()} onToggleTodo{toggleTodo} onDeleteTodo{deleteTodo} onUpdateTodoText{updateTodoText} / div classNamestats 总计: {todos.length} | 剩余: {todos.filter(t !t.completed).length} /div /div ); } export default App;在这个App组件中我们已经完成了最核心的状态提升。所有数据 (todos,filter) 和所有修改数据的方法都定义在这里。接下来子组件只需要通过 Props 接收它们需要的数据和函数即可。3.2 实现 TodoInput父子通信Props 回调TodoInput组件是一个典型的子组件向父组件通信的例子。它内部维护自己的输入状态但在提交时调用父组件传递下来的回调函数。// src/components/TodoInput.jsx import { useState } from react; import styles from ./TodoInput.module.css; // 假设使用 CSS Modules const TodoInput ({ onAddTodo }) { const [inputText, setInputText] useState(); const handleSubmit (e) { e.preventDefault(); const trimmedText inputText.trim(); if (!trimmedText) { alert(请输入内容); return; } // 关键步骤调用父组件传递下来的函数并传递数据 onAddTodo(trimmedText); setInputText(); // 清空输入框 }; return ( form onSubmit{handleSubmit} className{styles.form} input typetext value{inputText} onChange{(e) setInputText(e.target.value)} placeholder有什么需要完成的 className{styles.input} / button typesubmit className{styles.button}添加/button /form ); }; export default TodoInput;通信解析App通过onAddTodo{addTodo}将addTodo函数作为 Prop 传递给TodoInput。TodoInput在handleSubmit中调用props.onAddTodo(inputText)。这相当于子组件“发射”了一个事件并携带了数据新的 Todo 文本。父组件App中定义的addTodo函数被执行更新了顶层的todos状态。状态更新触发App重新渲染新的todos通过 Props 流向下游的TodoList和TodoItem。实操心得在表单提交时一定要e.preventDefault()阻止默认的页面刷新行为。这是 React 处理表单的常见做法。另外对输入值进行 trim 和空值校验是提升用户体验的必要步骤最好在子组件中完成保持回调接口的纯净。4. 列表渲染与组件间通信深化有了数据和输入接下来实现列表的展示和交互。4.1 实现 TodoList 与 TodoItem列表渲染与 Props 透传TodoList主要是一个展示组件它接收过滤后的todos数组然后进行遍历渲染。// src/components/TodoList.jsx import TodoItem from ./TodoItem; import styles from ./TodoList.module.css; const TodoList ({ todos, onToggleTodo, onDeleteTodo, onUpdateTodoText }) { if (todos.length 0) { return div className{styles.empty}暂无待办事项/div; } return ( ul className{styles.list} {todos.map(todo ( TodoItem key{todo.id} todo{todo} onToggle{onToggleTodo} onDelete{onDeleteTodo} onUpdateText{onUpdateTodoText} / ))} /ul ); }; export default TodoList;TodoItem是每个条目的展示和交互单元它接收一个todo对象和一系列操作函数。// src/components/TodoItem.jsx import { useState } from react; import styles from ./TodoItem.module.css; const TodoItem ({ todo, onToggle, onDelete, onUpdateText }) { const [isEditing, setIsEditing] useState(false); const [editText, setEditText] useState(todo.text); const handleSave () { const trimmed editText.trim(); if (trimmed trimmed ! todo.text) { onUpdateText(todo.id, trimmed); } else if (!trimmed) { // 如果编辑后为空可以视为删除或恢复原值这里我们恢复原值并退出编辑 setEditText(todo.text); } setIsEditing(false); }; const handleKeyDown (e) { if (e.key Enter) { handleSave(); } else if (e.key Escape) { setEditText(todo.text); setIsEditing(false); } }; return ( li className{${styles.item} ${todo.completed ? styles.completed : }} input typecheckbox checked{todo.completed} onChange{() onToggle(todo.id)} className{styles.checkbox} / {isEditing ? ( input typetext value{editText} onChange{(e) setEditText(e.target.value)} onBlur{handleSave} onKeyDown{handleKeyDown} className{styles.editInput} autoFocus / ) : ( span className{styles.text} onDoubleClick{() setIsEditing(true)} {todo.text} /span )} div className{styles.actions} {!isEditing ( button onClick{() setIsEditing(true)} className{styles.button}编辑/button button onClick{() onDelete(todo.id)} className{${styles.button} ${styles.delete}}删除/button / )} {isEditing ( button onClick{handleSave} className{styles.button}保存/button button onClick{() { setEditText(todo.text); setIsEditing(false); }} className{styles.button}取消/button / )} /div /li ); }; export default TodoItem;通信解析App将todos、onToggleTodo、onDeleteTodo、onUpdateTodoText传递给TodoList。TodoList遍历todos将每一个todo对象和对应的操作函数透传给每一个TodoItem实例。这里的TodoList像一个“二传手”。TodoItem内部管理着自己的编辑状态 (isEditing,editText)。当用户点击“完成”、“删除”或“保存编辑”时它调用从 Props 接收到的对应函数如onToggle(todo.id)从而向上修改App中的状态。这种模式被称为“回调函数作为 Props”或“渲染属性”的变体是 React 中最基础的组件通信方式。注意事项在TodoItem中实现编辑功能时状态管理容易出错。我建议遵循一个原则编辑态是 UI 状态保存在当前组件最终的结果是数据状态提交给父组件。所以editText在TodoItem中用useState管理保存时才调用onUpdateText更新全局状态。同时要处理好“保存”、“取消”、“按 ESC 退出”等多种交互并提供良好的用户体验如失焦保存、回车保存。4.2 实现 TodoFilter兄弟组件间的间接通信TodoFilter和TodoList是兄弟组件它们之间没有直接的 Props 传递。它们通过共同的父组件App进行通信这是处理兄弟组件通信的标准解法。// src/components/TodoFilter.jsx import { FILTER_STATUS } from ../App; import styles from ./TodoFilter.module.css; const TodoFilter ({ currentFilter, onFilterChange }) { const buttons [ { key: FILTER_STATUS.ALL, label: 全部 }, { key: FILTER_STATUS.ACTIVE, label: 进行中 }, { key: FILTER_STATUS.COMPLETED, label: 已完成 }, ]; return ( div className{styles.filter} {buttons.map(({ key, label }) ( button key{key} className{${styles.button} ${currentFilter key ? styles.active : }} onClick{() onFilterChange(key)} {label} /button ))} /div ); }; export default TodoFilter;通信解析App将当前筛选状态filter和设置函数setFilter以onFilterChange为名传递给TodoFilter。用户点击TodoFilter的按钮触发onClick调用onFilterChange(newFilterKey)。这实际上调用了App中的setFilter更新了App的filter状态。App重新渲染getFilteredTodos()根据新的filter计算出新的列表。新的列表 (getFilteredTodos()) 通过 Props 传递给TodoListTodoList随之重新渲染。至此TodoFilter通过改变父组件的状态间接影响了兄弟组件TodoList的显示内容。这种模式清晰且易于理解只要组件层级不深它是首选方案。5. 进阶通信使用 Context 进行跨层级通信当应用组件树变得复杂比如TodoItem深处某个孙组件需要直接访问主题色、用户信息或者我们想避免多层 Props 透传俗称“Props Drilling”时React 的Context就派上用场了。我们来为 Todo 应用添加一个简单的“主题切换”功能用 Context 实现。5.1 创建 ThemeContext首先创建一个 Context 来管理主题。// src/contexts/ThemeContext.jsx import { createContext, useState, useContext } from react; const ThemeContext createContext(); export const ThemeProvider ({ children }) { const [theme, setTheme] useState(light); // light 或 dark const toggleTheme () { setTheme(prev prev light ? dark : light); }; return ( ThemeContext.Provider value{{ theme, toggleTheme }} {children} /ThemeContext.Provider ); }; // 自定义 Hook方便使用 export const useTheme () { const context useContext(ThemeContext); if (!context) { throw new Error(useTheme must be used within a ThemeProvider); } return context; };5.2 在 App 顶层提供 Context在main.jsx或App.jsx外层包裹ThemeProvider。// src/main.jsx import React from react; import ReactDOM from react-dom/client; import App from ./App; import { ThemeProvider } from ./contexts/ThemeContext; import ./index.css; ReactDOM.createRoot(document.getElementById(root)).render( React.StrictMode ThemeProvider App / /ThemeProvider /React.StrictMode, );5.3 在深层子组件中消费 Context现在我们可以在任何子组件中直接使用主题而无需通过App一层层传递 Props。例如在TodoItem中根据主题改变样式并在App中添加一个切换主题的按钮。首先在App.jsx中添加一个切换按钮// src/App.jsx 中增加 import { useTheme } from ./contexts/ThemeContext; // ... 在 App 组件内部 const { theme, toggleTheme } useTheme(); // 在 return 的 JSX 中某处添加 button onClick{toggleTheme} className{theme-toggle ${theme}} 切换主题 ({theme}) /button然后在TodoItem.jsx中我们也可以直接消费主题// src/components/TodoItem.jsx import { useTheme } from ../contexts/ThemeContext; // ... 在 TodoItem 组件内部 const { theme } useTheme(); // 在 JSX 中动态添加类名 li className{${styles.item} ${todo.completed ? styles.completed : } ${theme}} {/* ... */} /li通信解析ThemeProvider在组件树顶层提供了一个“值”{ theme, toggleTheme }。任何在其下的子组件如App、TodoItem无论嵌套多深只要调用useTheme()Hook就能直接读取到最新的theme值和toggleTheme函数无需中间组件传递。当在App中调用toggleTheme时ThemeProvider内部的theme状态更新导致所有消费了useTheme()的组件App和TodoItem都会重新渲染并获取到新的theme值。实操心得Context 非常适合用于传递全局的、变化不频繁的数据如主题、用户认证信息、语言偏好等。但它不是状态管理库的替代品。当 Context 中存储的值频繁变化时比如我们如果把整个todos放到 Context 里会导致所有消费该 Context 的组件都重新渲染可能带来性能问题。对于频繁变化的复杂状态更好的选择是useReducer Context或者使用专门的状态管理库。6. 状态管理进阶useReducer 与 Context 结合对于 TodoList 这种具有多种操作类型ADD, TOGGLE, DELETE, UPDATE的状态逻辑使用useReducer比一堆独立的useState更清晰逻辑更集中。我们可以将App中的状态和操作重构到useReducer中甚至结合 Context 提供一个更优雅的全局状态管理方案。6.1 使用 useReducer 重构 App 状态首先定义 reducer 逻辑。// src/reducers/todoReducer.js export const TODO_ACTIONS { ADD: ADD_TODO, TOGGLE: TOGGLE_TODO, DELETE: DELETE_TODO, UPDATE_TEXT: UPDATE_TODO_TEXT, SET_FILTER: SET_FILTER }; export const initialState { todos: [], filter: all }; export const todoReducer (state, action) { switch (action.type) { case TODO_ACTIONS.ADD: return { ...state, todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }] }; case TODO_ACTIONS.TOGGLE: return { ...state, todos: state.todos.map(todo todo.id action.payload.id ? { ...todo, completed: !todo.completed } : todo ) }; case TODO_ACTIONS.DELETE: return { ...state, todos: state.todos.filter(todo todo.id ! action.payload.id) }; case TODO_ACTIONS.UPDATE_TEXT: return { ...state, todos: state.todos.map(todo todo.id action.payload.id ? { ...todo, text: action.payload.text } : todo ) }; case TODO_ACTIONS.SET_FILTER: return { ...state, filter: action.payload.filter }; default: return state; } };然后在App.jsx中使用useReducer。// src/App.jsx (重构版) import { useReducer } from react; import { todoReducer, initialState, TODO_ACTIONS } from ./reducers/todoReducer; // ... 其他导入 function App() { const [state, dispatch] useReducer(todoReducer, initialState); const { todos, filter } state; const addTodo (text) { dispatch({ type: TODO_ACTIONS.ADD, payload: { text } }); }; const toggleTodo (id) { dispatch({ type: TODO_ACTIONS.TOGGLE, payload: { id } }); }; // ... 其他 action creator 函数类似 // getFilteredTodos 函数保持不变但使用 state.todos 和 state.filter const getFilteredTodos () { /* ... */ }; return ( // ... JSX 部分基本不变只是将原来的 setTodos 等替换为 dispatch 调用 ); }6.2 创建 TodoContext 提供全局状态更进一步我们可以创建一个专门的 Context 来提供 Todo 相关的状态和 dispatch 函数让深层组件无需 Props 钻取就能直接操作 Todo。// src/contexts/TodoContext.jsx import { createContext, useReducer, useContext } from react; import { todoReducer, initialState } from ../reducers/todoReducer; const TodoContext createContext(); export const TodoProvider ({ children }) { const [state, dispatch] useReducer(todoReducer, initialState); // 可选将 dispatch 封装成更易用的 actions const actions { addTodo: (text) dispatch({ type: ADD_TODO, payload: { text } }), toggleTodo: (id) dispatch({ type: TOGGLE_TODO, payload: { id } }), // ... 其他 actions }; return ( TodoContext.Provider value{{ state, dispatch, actions }} {children} /TodoContext.Provider ); }; export const useTodo () { const context useContext(TodoContext); if (!context) { throw new Error(useTodo must be used within a TodoProvider); } return context; };在main.jsx中包裹TodoProvider然后App及所有子组件都可以使用useTodo来获取状态和派发动作。这样TodoItem组件就可以直接从 Context 中读取actions.toggleTodo而不需要从TodoList透传下来。模式对比Props 传递简单直接数据流清晰可见适合层级不深、关系明确的组件。Context解决跨层级传递问题适合全局、低频更新的数据。useReducer Context将状态逻辑集中管理通过 Context 提供全局访问是中型应用在不引入 Redux 等库时的一个不错选择。它结合了useReducer的可预测性和Context的穿透性。7. 常见问题、性能优化与排查技巧在实际开发中你会遇到各种问题和性能考量。这里记录一些典型场景和解决方案。7.1 列表渲染性能与key的陷阱问题当todos列表很大时每次状态更新导致整个列表重渲染可能造成卡顿。或者在操作列表如删除、排序时出现奇怪的 UI 错误。分析与解决正确的key在TodoList中渲染TodoItem时key{todo.id}至关重要。React 依靠key来识别列表中的每个元素从而高效地更新 DOM。绝对不要用数组索引index作为key尤其是在列表项可能被排序、过滤、增删的情况下这会导致渲染错误和性能下降。使用稳定且唯一的id。组件记忆化如果TodoItem组件渲染开销大可以使用React.memo进行包裹。React.memo会对组件 Props 进行浅比较如果 Props 没变则跳过渲染。const TodoItem React.memo(({ todo, onToggle, onDelete, onUpdateText }) { // ... 组件实现 });但要注意如果传递给它的回调函数如onToggle在父组件每次渲染时都是新的引用React.memo就会失效。这时需要结合useCallback来稳定回调函数的引用。回调函数稳定性在App组件中使用useCallback来记忆化那些传递给子组件的函数。const toggleTodo useCallback((id) { dispatch({ type: TODO_ACTIONS.TOGGLE, payload: { id } }); }, [dispatch]); // dispatch 来自 useReducer通常是稳定的这样toggleTodo函数的引用在依赖项不变的情况下保持不变使得被React.memo包裹的TodoItem能正确跳过不必要的渲染。7.2 状态更新异步性与函数式更新问题当你连续快速操作状态时有时会发现状态不是最新的。分析与解决React 的状态更新是异步的。在事件处理函数中连续调用setState可能不会基于前一个调用的结果。对于useState应该使用函数式更新来确保基于最新状态。// 不安全的做法如果 rapidClicks 被快速调用多次 const [count, setCount] useState(0); const rapidClicks () { setCount(count 1); setCount(count 1); // 这里拿到的 count 可能还是旧的 }; // 安全的做法使用函数式更新 const rapidClicksSafe () { setCount(prevCount prevCount 1); setCount(prevCount prevCount 1); // 总能拿到前一次更新后的值 };在我们的todoReducer中我们已经遵循了纯函数的原则总是返回一个新的状态对象这本身就是一种“函数式”的更新因此不存在这个问题。但在使用useState处理复杂状态时要特别注意。7.3 表单输入与受控组件问题输入框有时会卡顿或者输入不跟手。分析与解决我们使用了“受控组件”模式即输入框的值 (value) 由 React 状态 (inputText) 控制并通过onChange事件更新状态。这是 React 推荐的做法因为它使得状态成为唯一数据源。但在极端情况下频繁的setState和重渲染可能导致性能问题。对于简单的表单这通常不是问题。如果遇到性能瓶颈可以考虑使用useDeferredValue(React 18) 或debounce来延迟更新非关键的状态。对于非常复杂的表单评估是否真的需要每个字符都受控有时在onBlur时再更新状态也是可接受的称为“非受控组件”的一种形式。7.4 网络请求与副作用管理问题如何从后端 API 加载初始 Todo 列表或保存修改分析与解决使用useEffectHook。例如在App或TodoProvider中加载初始数据import { useEffect } from react; import { useTodo } from ./contexts/TodoContext; const TodoApp () { const { dispatch } useTodo(); useEffect(() { const fetchTodos async () { try { const response await fetch(/api/todos); const data await response.json(); dispatch({ type: LOAD_TODOS, payload: data }); } catch (error) { console.error(Failed to fetch todos:, error); } }; fetchTodos(); }, [dispatch]); // 依赖项 dispatch 通常是稳定的 // ... 其余代码 };重要提示记得在useEffect的清理函数中取消未完成的请求以防止内存泄漏和“在已卸载组件上更新状态”的警告。7.5 调试技巧React Developer Tools这是最强大的调试工具。安装浏览器扩展后你可以检查组件树查看每个组件的 Props、State、Hooks。分析渲染性能使用 Profiler 标签页记录一次交互查看哪些组件渲染了、渲染耗时找出不必要的渲染。跟踪状态更新在组件上设置“Break on”某个 state 或 prop 的变化方便调试。8. 拥抱 React 19探索新特性最后让我们展望一下如何用 React 19 的一些新特性来让我们的 Todo 应用更优雅。8.1 使用useHook 处理异步初始状态假设我们的初始todos来自一个异步函数fetchInitialTodos()。在 React 19 之前我们需要在useEffect中处理并管理加载和错误状态。useHook 提供了一种更直接的方式注意此为实验性 API。// 假设在 TodoProvider 中 import { use } from react; // 注意这是实验性的 function TodoProvider({ children }) { // 使用 use 来“消费”一个 Promise const initialTodosPromise fetchInitialTodos(); // 返回 Promise let initialState; try { // use 会挂起组件直到 Promise 解决 const fetchedTodos use(initialTodosPromise); initialState { todos: fetchedTodos, filter: all }; } catch (error) { // 处理错误可以设置一个错误状态 initialState { todos: [], filter: all, error }; } const [state, dispatch] useReducer(todoReducer, initialState); // ... 其余代码 }这种方式让异步数据获取更像同步代码但需要搭配支持 Suspense 的框架使用。8.2 使用 Actions API 重构表单提交React 19 为表单引入了更强大的 Actions 模式。我们的TodoInput可以重构如下// 使用 React 19 Actions (示例API可能变化) import { useActionState } from react; // 或 useFormState function TodoInput({ onAddTodo }) { const [inputText, setInputText] useState(); // useActionState 接收一个 action 函数和初始状态 const [state, formAction, isPending] useActionState( async (previousState, formData) { const text formData.get(todoText); await onAddTodo(text); // 假设 onAddTodo 现在是异步的 return { message: Added! }; // 可以返回一些结果状态 }, null ); return ( form action{formAction} input typetext nametodoText value{inputText} onChange{(e) setInputText(e.target.value)} / button typesubmit disabled{isPending} {isPending ? 添加中... : 添加} /button {state p{state.message}/p} /form ); }Actions API 简化了表单状态管理pending, error, data并更好地集成了异步操作。8.3 编译器优化与心智模型React 19 的编译器如 React Forget旨在自动记忆化Memoization。这意味着未来我们可能不再需要手动写useMemo和useCallback来优化性能编译器会帮我们分析并自动添加。作为开发者我们应该继续编写符合 React 范式纯组件、稳定的依赖项的代码并信任编译器会进行优化。但这并不意味着性能优化不再重要而是将精力更多放在减少不必要的状态更新和组件分割上。从零搭建这个 Todo List 的过程本质上是一个对 React 核心思想——组件化、状态驱动、单向数据流——的深度实践。通过 Props、Context、Reducer 等多种模式的组合我们构建了一个清晰、可维护的应用架构。React 19 的新特性正在让这些模式变得更加简洁和强大。记住没有一种通信方式是万能的关键在于根据组件的层级关系、数据流动的频次和复杂度选择最合适的那一种。当你对它们都了然于胸时面对任何复杂的页面你都能游刃有余地拆解和设计。