当前位置: 首页 > news >正文

React 19组件通信实战:从零构建Todo List应用

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 的“单一职责原则”。

  1. App组件:应用的根组件,它不负责具体的 UI 渲染,而是作为状态管理中心和数据流的中枢。它持有最顶层的状态(如所有的 Todo 项),并定义修改这些状态的方法。然后通过 Props 将这些状态和方法“注入”到子组件中。
  2. TodoInput组件:一个受控的输入框组件。它的职责非常纯粹:接收用户的输入,并在用户点击“添加”按钮或按回车键时,将新的待办事项文本提交给父组件(App)。它自身不管理 Todo 列表。
  3. TodoList组件:负责渲染 Todo 项列表。它接收来自Apptodos数组和过滤状态,根据条件筛选后,将每个 Todo 项的数据传递给子组件TodoItem进行渲染。它像一个列表的“容器”或“视图”。
  4. TodoItem组件:代表单个待办事项的展示单元。它接收一个todo对象作为 Prop,展示其内容、完成状态,并提供“完成/取消”、“编辑”、“删除”等操作的 UI。当用户触发这些操作时,它需要调用父组件(通过 Props 传递下来的方法)来更新数据。
  5. TodoFilter组件:提供“全部”、“进行中”、“已完成”等筛选按钮。点击不同按钮时,它需要通知App组件当前选中的过滤条件,从而驱动TodoList重新渲染过滤后的列表。

这个架构的核心思想是:状态上提。所有可变的数据(todosfilter)都存放在最接近的共同祖先组件——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 内置的useStateuseReducer以及 Context 来完成所有通信。在项目后期,我们会讨论何时需要考虑引入这些库。
  • 样式方案:为了简洁,我们使用普通的 CSS Modules 或简单的内联样式。这不会分散我们对逻辑的注意力。
  • React 19 新特性实践
    • useHook:这是一个实验性但非常有前景的 Hook,用于更优雅地处理异步数据(如从模拟 API 读取初始 Todos)。我们可以在数据加载部分尝试使用它,但需要明确它尚未稳定。
    • Actions API:React 19 为表单处理引入了更强大的 Actions 概念,与useFormStatususeFormState等 Hook 配合。我们的TodoInput本质上是一个表单,可以尝试用新的 Actions 模式重构,体验声明式数据提交。
    • 编译器优化:我们不需要手动做任何事情,但要知道 React 19 的编译器(如 React Forget)旨在自动生成等效于useMemouseCallback的代码,这可能会影响我们性能优化的习惯。在编写代码时,我们仍会按需使用这些 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.jsxsrc/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
  • TodoInputhandleSubmit中调用props.onAddTodo(inputText)
  • 这相当于子组件“发射”了一个事件,并携带了数据(新的 Todo 文本)。父组件App中定义的addTodo函数被执行,更新了顶层的todos状态。
  • 状态更新触发App重新渲染,新的todos通过 Props 流向下游的TodoListTodoItem

实操心得:在表单提交时,一定要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;

通信解析

  • ApptodosonToggleTodoonDeleteTodoonUpdateTodoText传递给TodoList
  • TodoList遍历todos,将每一个todo对象和对应的操作函数透传给每一个TodoItem实例。这里的TodoList像一个“二传手”。
  • TodoItem内部管理着自己的编辑状态 (isEditing,editText)。当用户点击“完成”、“删除”或“保存编辑”时,它调用从 Props 接收到的对应函数(如onToggle(todo.id)),从而向上修改App中的状态。
  • 这种模式被称为“回调函数作为 Props”或“渲染属性”的变体,是 React 中最基础的组件通信方式。

注意事项:在TodoItem中实现编辑功能时,状态管理容易出错。我建议遵循一个原则:编辑态是 UI 状态,保存在当前组件;最终的结果是数据状态,提交给父组件。所以editTextTodoItem中用useState管理,保存时才调用onUpdateText更新全局状态。同时,要处理好“保存”、“取消”、“按 ESC 退出”等多种交互,并提供良好的用户体验(如失焦保存、回车保存)。

4.2 实现 TodoFilter:兄弟组件间的间接通信

TodoFilterTodoList是兄弟组件,它们之间没有直接的 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;

通信解析

  1. App将当前筛选状态filter和设置函数setFilter(以onFilterChange为名)传递给TodoFilter
  2. 用户点击TodoFilter的按钮,触发onClick,调用onFilterChange(newFilterKey)
  3. 这实际上调用了App中的setFilter,更新了Appfilter状态。
  4. App重新渲染,getFilteredTodos()根据新的filter计算出新的列表。
  5. 新的列表 (getFilteredTodos()) 通过 Props 传递给TodoListTodoList随之重新渲染。
  6. 至此,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.jsxApp.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 })。
  • 任何在其下的子组件(如AppTodoItem),无论嵌套多深,只要调用useTheme()Hook,就能直接读取到最新的theme值和toggleTheme函数,无需中间组件传递。
  • 当在App中调用toggleTheme时,ThemeProvider内部的theme状态更新,导致所有消费了useTheme()的组件(AppTodoItem)都会重新渲染,并获取到新的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 错误。

分析与解决

  1. 正确的key:在TodoList中渲染TodoItem时,key={todo.id}至关重要。React 依靠key来识别列表中的每个元素,从而高效地更新 DOM。绝对不要用数组索引index作为key,尤其是在列表项可能被排序、过滤、增删的情况下,这会导致渲染错误和性能下降。使用稳定且唯一的id
  2. 组件记忆化:如果TodoItem组件渲染开销大,可以使用React.memo进行包裹。React.memo会对组件 Props 进行浅比较,如果 Props 没变,则跳过渲染。
    const TodoItem = React.memo(({ todo, onToggle, onDelete, onUpdateText }) => { // ... 组件实现 });
    但要注意,如果传递给它的回调函数(如onToggle)在父组件每次渲染时都是新的引用,React.memo就会失效。这时需要结合useCallback来稳定回调函数的引用。
  3. 回调函数稳定性:在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和重渲染可能导致性能问题。对于简单的表单,这通常不是问题。如果遇到性能瓶颈,可以考虑:

  1. 使用useDeferredValue(React 18+) 或debounce来延迟更新非关键的状态。
  2. 对于非常复杂的表单,评估是否真的需要每个字符都受控,有时在onBlur时再更新状态也是可接受的(称为“非受控组件”的一种形式)。

7.4 网络请求与副作用管理

问题:如何从后端 API 加载初始 Todo 列表,或保存修改?

分析与解决:使用useEffectHook。例如,在AppTodoProvider中加载初始数据:

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)。这意味着,未来我们可能不再需要手动写useMemouseCallback来优化性能,编译器会帮我们分析并自动添加。作为开发者,我们应该继续编写符合 React 范式(纯组件、稳定的依赖项)的代码,并信任编译器会进行优化。但这并不意味着性能优化不再重要,而是将精力更多放在减少不必要的状态更新和组件分割上。

从零搭建这个 Todo List 的过程,本质上是一个对 React 核心思想——组件化、状态驱动、单向数据流——的深度实践。通过 Props、Context、Reducer 等多种模式的组合,我们构建了一个清晰、可维护的应用架构。React 19 的新特性正在让这些模式变得更加简洁和强大。记住,没有一种通信方式是万能的,关键在于根据组件的层级关系、数据流动的频次和复杂度,选择最合适的那一种。当你对它们都了然于胸时,面对任何复杂的页面,你都能游刃有余地拆解和设计。

http://www.cnnetsun.cn/news/4011835.html

相关文章:

  • 学校网站怎么建设:从规划到落地的全案解析,助你打造高效教育门户
  • 在数字洪流中重塑价值:深度解析麓谷网站建设的核心逻辑与未来趋势,助力企业突破流量瓶颈
  • GPT-6技术前瞻:从MoE架构革新到多模态AGI的演进路径
  • 为什么很多人说专业网站建设比较好?深度解析企业数字化转型背后的真相与误区
  • 解决Java AES-256加密Illegal key size异常:JCE策略与Bouncy Castle实战
  • LangChain实战:从核心概念到生产级AI应用开发指南
  • 移动终端网站建设:在这个指尖滑动的时代,你的网站真的“动”起来了吗
  • 揭秘广州网站建设外包背后的真相与避坑指南
  • 像淘宝购物网站建设需要哪些专业人员
  • 2024年企业转型刚需解析:杭州网站建设网如何助力品牌突围与流量增长策略深度复盘
  • 从“未激活弹窗“到180天省心授权:KMS_VL_ALL_AIO智能激活脚本完整上手手册
  • 揭秘深圳外贸网站建设公司背后的那些事儿与行业真相
  • 网站业务建设是什么意思深度解析从底层逻辑到落地实战的全景指南
  • 建设网站需要什么设施:新手避坑指南与全能解析
  • 如何快速搭建Nintendo Switch大气层系统:完整破解与安全使用指南
  • 网站建设的经济可行性:中小企业如何算好这笔账并实现低成本高回报
  • 揭秘上饶建设银行网站如何成为当地人生活必备神器以及那些你不知道的隐藏功能
  • 2024年企业数字化转型关键!深度解析云一网站建设如何助力品牌高效落地
  • 手搓开源AI编程助手:基于DeepSeek-Coder的轻量级Claude Code复刻实践
  • 汇鑫小学网站建设怎么做才能让孩子和家长都爱看?揭秘打造高颜值校园网的实战心得
  • 国企网站建设要求揭秘:从政治站位到用户体验的全面合规指南
  • 网站建设过程中那些让你头秃又上瘾的英语词汇深度指南
  • 大语言模型API返回JSON解析全攻略:从防御性解析到Prompt工程
  • AI Agent懒加载行动说明:优化Claude Code技能系统架构设计
  • 揭秘万网网站建设方案书范文:中小企业数字化转型的实战指南与避坑指南
  • 揭秘企业网站建设基本原则:如何打造高转化、高口碑的官方门户
  • 网站建设开发进度表:从零到上线的全流程指南与核心节点把控
  • 揭秘即墨网站建设哪家好:从避坑指南到实战攻略,手把手教你找到最懂你的设计团队
  • 揭秘西充县住房和城乡规划建设局网站背后的民生温度与城市发展脉搏
  • 揭秘网站建设方案word文档模板与核心逻辑:中小企业如何低成本构建数字化名片