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重新渲染过滤后的列表。
这个架构的核心思想是:状态上提。所有可变的数据(todos,filter)都存放在最接近的共同祖先组件——App中。子组件大多是“纯”的展示组件或简单的交互组件,它们通过 Props 接收数据和回调函数。当子组件需要修改数据时,调用这些回调函数,由App来执行实际的 state 更新。这样,数据流是单向且可预测的:从App流向子组件,子组件的动作通过回调函数“冒泡”回App。
2.2 技术选型与 React 19 新特性考量
对于这个项目,我们坚持使用最核心的 React 技术栈,以聚焦于组件通信本身。
- 构建工具:使用
Vite。它启动快、配置简单,对 React 的支持开箱即用,能让我们快速进入开发。命令npm create vite@latest my-todo-app -- --template react即可搭建。 - 状态管理:暂不使用 Redux、MobX 等第三方库。我们的首要目标是熟练掌握 React 内置的
useState、useReducer以及 Context 来完成所有通信。在项目后期,我们会讨论何时需要考虑引入这些库。 - 样式方案:为了简洁,我们使用普通的 CSS Modules 或简单的内联样式。这不会分散我们对逻辑的注意力。
- React 19 新特性实践:
useHook:这是一个实验性但非常有前景的 Hook,用于更优雅地处理异步数据(如从模拟 API 读取初始 Todos)。我们可以在数据加载部分尝试使用它,但需要明确它尚未稳定。- Actions API:React 19 为表单处理引入了更强大的 Actions 概念,与
useFormStatus、useFormState等 Hook 配合。我们的TodoInput本质上是一个表单,可以尝试用新的 Actions 模式重构,体验声明式数据提交。 - 编译器优化:我们不需要手动做任何事情,但要知道 React 19 的编译器(如 React Forget)旨在自动生成等效于
useMemo和useCallback的代码,这可能会影响我们性能优化的习惯。在编写代码时,我们仍会按需使用这些 Hook 来讲解概念,但心里要明白未来的趋势。
注意:React 19 的某些新特性可能仍处于 Canary 或实验性频道。为了稳定性,我们的核心代码将基于稳定版的模式编写,但会开辟独立章节来探讨如何用新特性进行重构和优化。
3. 基础搭建与核心状态管理
让我们开始动手。首先初始化项目并搭建起最基础的状态管理结构。
3.1 项目初始化与组件骨架
使用 Vite 快速创建项目:
npm create vite@latest 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 className="app"> <h1>⚛️ React 19 Todo List</h1> <TodoInput onAddTodo={addTodo} /> <TodoFilter currentFilter={filter} onFilterChange={setFilter} /> <TodoList todos={getFilteredTodos()} onToggleTodo={toggleTodo} onDeleteTodo={deleteTodo} onUpdateTodoText={updateTodoText} /> <div className="stats"> 总计: {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 type="text" value={inputText} onChange={(e) => setInputText(e.target.value)} placeholder="有什么需要完成的?" className={styles.input} /> <button type="submit" 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 type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} className={styles.checkbox} /> {isEditing ? ( <input type="text" 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 传递给TodoList,TodoList随之重新渲染。 - 至此,
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 type="text" name="todoText" value={inputText} onChange={(e) => setInputText(e.target.value)} /> <button type="submit" 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 的新特性正在让这些模式变得更加简洁和强大。记住,没有一种通信方式是万能的,关键在于根据组件的层级关系、数据流动的频次和复杂度,选择最合适的那一种。当你对它们都了然于胸时,面对任何复杂的页面,你都能游刃有余地拆解和设计。