ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

React 19组件通信实战:从父子到深层嵌套,构建现代化Todo List应用

React 19组件通信实战:从父子到深层嵌套,构建现代化Todo List应用 1. 项目概述为什么 Todo List 是 React 学习的“圣杯”如果你正在学习 React或者想通过一个项目来检验自己对 React 19 新特性的掌握程度那么从零搭建一个 Todo List 应用绝对是一个经典且高效的选择。这听起来可能有些老生常谈但请别急着划走。这个看似简单的项目实际上是一个微缩的、完整的前端应用模型它几乎涵盖了现代 React 开发中所有核心的思维模式和工程实践状态管理、组件拆分、数据流设计、副作用处理以及最关键的——组件通信。我见过很多开发者他们能背诵 React 的 API却在实际项目中面对复杂的组件关系时感到无从下手。问题的核心往往不在于 API 本身而在于对“数据如何流动”和“组件如何对话”缺乏系统性的理解。而 Todo List 这个项目恰恰是理解这一切的绝佳沙盘。在 React 19 的语境下我们有了更多现代化的工具如 Actions、useOptimistic 等来更优雅地解决这些问题。本次实战我将带你从零开始不仅搭建一个功能完整的 Todo List更重要的是我会深入剖析每一步背后的设计决策让你彻底吃透从父子通信、兄弟通信到深层嵌套组件通信的完整解决方案并融入 React 19 的最新实践。无论你是 React 新手想夯实基础还是有一定经验的开发者想了解最新特性这篇实战指南都将提供一条清晰的路径。2. 项目整体设计与核心思路拆解2.1 功能需求与技术选型一个完整的 Todo List 应用远不止是“添加”和“删除”那么简单。为了充分演练组件通信我们设计以下核心功能模块任务列表展示显示所有待办事项每条任务包含内容、完成状态。添加新任务通过输入框和按钮添加任务。标记任务完成/未完成点击复选框切换任务状态通常伴有视觉反馈如删除线。删除任务移除不再需要的任务。任务筛选根据“全部”、“进行中”、“已完成”来过滤显示的任务列表。状态持久化刷新页面后任务列表不丢失可选用于演练副作用。基于这些功能我们的技术栈非常明确框架React 19。我们将使用其最新的稳定特性。构建工具Vite。它启动快、配置简单是当前 React 项目的事实标准。语言TypeScript。类型系统能极大提升代码的健壮性和开发体验尤其在定义组件间传递的 props 时。样式方案CSS Modules 或 Tailwind CSS。为了聚焦逻辑我们选择简单的 CSS Modules避免样式耦合的干扰。这个选型组合代表了当前 React 社区最主流、最前沿的实践能确保我们的项目既具学习价值又贴近真实工作环境。2.2 组件化架构设计组件化是 React 的核心思想。在设计之初我们就需要思考如何将 UI 拆分为独立、可复用的部分。下图清晰地展示了我们应用的组件树结构及主要的数据流方向App (根组件持有核心状态 todos) ├── TodoInput (新增任务向App提交新任务) ├── TodoFilter (筛选控件改变App中的筛选状态) └── TodoList (任务列表展示) └── TodoItem (单个任务项) ├── TodoCheckbox (完成状态切换向上冒泡状态变更) ├── TodoText (任务内容展示与编辑) └── TodoDeleteButton (删除当前任务)设计思路解析状态提升todos任务列表和filter筛选条件这两个最核心的状态被提升到共同的父组件App中。这是因为TodoList、TodoFilter都需要访问或修改它们。这是 React 中最基础的组件通信模式——通过 props 向下传递数据和回调函数。职责单一每个组件只做一件事。TodoInput只负责输入和提交TodoItem只负责渲染单条任务和派发动作TodoCheckbox只处理点击事件。这使得每个组件都易于理解、测试和维护。数据流向清晰数据从App向下流经TodoList到TodoItem。用户的交互动作如点击删除按钮则通过回调函数沿着组件树一层层“冒泡”回App由App更新状态从而触发整个 UI 的重新渲染。这种单向数据流使得应用的行为变得可预测。这个架构是我们后续所有通信模式演练的蓝图。理解它就理解了整个应用的运作机理。3. 从零搭建开发环境与项目初始化3.1 使用 Vite 快速初始化项目我们首先使用最快捷的方式搭建开发环境。确保你的 Node.js 版本在 18。打开终端执行以下命令# 使用 npm 创建 Vite React TypeScript 项目 npm create vitelatest react19-todo-list -- --template react-ts # 进入项目目录 cd react19-todo-list # 安装依赖 npm install这个过程会创建一个标准的 React TypeScript 项目结构。接下来我们清理一下默认文件并创建我们的组件结构# 清理 src 目录保留必要的入口文件 rm src/App.css src/index.css src/App.tsx # 创建我们的组件目录和文件 mkdir -p src/components touch src/components/TodoInput.tsx touch src/components/TodoFilter.tsx touch src/components/TodoList.tsx touch src/components/TodoItem.tsx touch src/components/TodoCheckbox.tsx touch src/components/TodoText.tsx touch src/components/TodoDeleteButton.tsx # 创建类型定义和工具函数文件 touch src/types/todo.ts touch src/utils/helper.ts3.2 定义核心数据类型与初始状态在深入组件之前我们先定义清晰的数据结构。这是 TypeScript 带来的首要优势也是确保组件间通信“语言一致”的基础。打开src/types/todo.ts// 定义单个待办事项的类型 export interface TodoItem { id: string; // 使用唯一ID避免用索引做key text: string; completed: boolean; createdAt: number; // 时间戳可用于排序 } // 定义筛选类型 export type FilterType all | active | completed;接着我们创建App.tsx的初始版本并定义状态// src/App.tsx import { useState } from react; import { TodoItem, FilterType } from ./types/todo; import TodoInput from ./components/TodoInput; import TodoFilter from ./components/TodoFilter; import TodoList from ./components/TodoList; import ./App.css; // 我们稍后创建 // 初始化的任务数据 const initialTodos: TodoItem[] [ { id: 1, text: 学习 React 19 新特性, completed: false, createdAt: Date.now() }, { id: 2, text: 吃透组件通信原理, completed: true, createdAt: Date.now() - 86400000 }, { id: 3, text: 完成 TodoList 实战项目, completed: false, createdAt: Date.now() - 172800000 }, ]; function App() { // 核心状态任务列表 const [todos, setTodos] useStateTodoItem[](initialTodos); // 核心状态当前筛选条件 const [filter, setFilter] useStateFilterType(all); // 根据筛选条件过滤任务 const filteredTodos todos.filter(todo { if (filter active) return !todo.completed; if (filter completed) return todo.completed; return true; // all }); return ( div classNameapp-container h1React 19 Todo List/h1 TodoInput onAddTodo{(text) {/* 待实现 */}} / TodoFilter currentFilter{filter} onFilterChange{setFilter} / TodoList todos{filteredTodos} onTodoChange{setTodos} / /div ); } export default App;同时创建简单的src/App.css.app-container { max-width: 600px; margin: 2rem auto; padding: 2rem; background: #f8f9fa; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; } h1 { color: #333; text-align: center; margin-bottom: 1.5rem; }现在项目的骨架和核心状态已经就位。接下来我们将逐一实现各个子组件并深入其中的通信细节。4. 组件通信模式深度解析与实现4.1 模式一父子通信 - Props 向下传递回调向上传递这是最基础、最常用的通信模式。父组件通过props将数据传递给子组件子组件通过调用props中传入的回调函数将信息“传回”给父组件。实例TodoInput 组件TodoInput需要将用户输入的新任务文本传递回App组件。App组件需要提供一个“添加任务”的函数给它。// src/components/TodoInput.tsx import { useState, FormEvent } from react; import styles from ./TodoInput.module.css; // 假设使用 CSS Modules interface TodoInputProps { onAddTodo: (text: string) void; // 关键父组件传入的回调函数 } const TodoInput ({ onAddTodo }: TodoInputProps) { const [inputText, setInputText] useState(); const handleSubmit (e: FormEvent) { e.preventDefault(); const trimmedText inputText.trim(); if (!trimmedText) 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.tsx中我们需要实现onAddTodo的具体逻辑// 在 App 组件内部 const handleAddTodo (text: string) { const newTodo: TodoItem { id: todo-${Date.now()}, // 生成简单唯一ID text, completed: false, createdAt: Date.now(), }; setTodos(prevTodos [newTodo, ...prevTodos]); // 使用函数式更新将新任务添加到前面 }; // 在 JSX 中传递给 TodoInput TodoInput onAddTodo{handleAddTodo} /通信要点单向性数据从App(todos) 流向TodoList事件从TodoInput流向App。回调函数是桥梁onAddTodo这个 prop 本质上是一个“通道”子组件通过它通知父组件发生了某事。类型安全TodoInputProps明确定义了onAddTodo必须是一个接收string参数的函数这在协作开发中能提前发现错误。4.2 模式二兄弟组件通信 - 状态提升TodoFilter筛选器和TodoList列表是兄弟组件它们之间没有直接的父子关系。它们需要共享同一个状态当前的筛选条件filter。解决方案是“状态提升”将共享状态filter提升到它们共同的父组件App中。App负责管理这个状态然后通过 props 分别传递给TodoFilter和TodoList。实例TodoFilter 组件// src/components/TodoFilter.tsx import { FilterType } from ../types/todo; import styles from ./TodoFilter.module.css; interface TodoFilterProps { currentFilter: FilterType; onFilterChange: (filter: FilterType) void; } const TodoFilter ({ currentFilter, onFilterChange }: TodoFilterProps) { const filters: { key: FilterType; label: string }[] [ { key: all, label: 全部 }, { key: active, label: 进行中 }, { key: completed, label: 已完成 }, ]; return ( div className{styles.filterGroup} {filters.map(({ key, label }) ( button key{key} className{${styles.filterButton} ${currentFilter key ? styles.active : }} onClick{() onFilterChange(key)} // 通知父组件状态变更 {label} /button ))} /div ); }; export default TodoFilter;在App.tsx中我们已经定义了filter状态和setFilter函数并传递给了TodoFilter。同时TodoList接收的是根据filter计算后的filteredTodos。// App.tsx 中的 JSX 片段 TodoFilter currentFilter{filter} onFilterChange{setFilter} / TodoList todos{filteredTodos} onTodoChange{setTodos} /通信要点共同父组件是协调者App充当了通信中枢。TodoFilter修改filterApp状态更新进而导致TodoList接收到的filteredTodos发生变化并重新渲染。避免了直接耦合TodoFilter和TodoList彼此不知道对方的存在它们只与父组件通信。这使得每个组件更容易被单独理解和测试。4.3 模式三深层嵌套组件通信 - Context 与 useReducer想象一下如果我们的TodoItem内部又嵌套了多层组件如TodoCheckbox、TodoText、TodoDeleteButton而这些深层组件都需要修改todos状态。如果一直通过 props 层层传递回调函数会导致“prop drilling”属性钻取问题代码变得冗长且难以维护。React 提供了Context API来解决这个问题。它可以跨组件层级共享数据而无需显式地通过 props 逐层传递。结合useReducerHook 来管理复杂状态逻辑是更优雅的方案。步骤1创建 Context 和 Reducer首先我们创建一个全局的 Context 来管理todos状态和所有操作。// src/contexts/TodoContext.tsx import React, { createContext, useContext, useReducer, ReactNode } from react; import { TodoItem } from ../types/todo; // 定义 Action 类型 type TodoAction | { type: ADD_TODO; payload: string } | { type: TOGGLE_TODO; payload: string } // payload 是 todo.id | { type: DELETE_TODO; payload: string } | { type: EDIT_TODO; payload: { id: string; text: string } }; // Reducer 函数根据不同的 action 类型返回新的 state const todoReducer (state: TodoItem[], action: TodoAction): TodoItem[] { switch (action.type) { case ADD_TODO: const newTodo: TodoItem { id: todo-${Date.now()}, text: action.payload, completed: false, createdAt: Date.now(), }; return [newTodo, ...state]; case TOGGLE_TODO: return state.map(todo todo.id action.payload ? { ...todo, completed: !todo.completed } : todo ); case DELETE_TODO: return state.filter(todo todo.id ! action.payload); case EDIT_TODO: return state.map(todo todo.id action.payload.id ? { ...todo, text: action.payload.text } : todo ); default: return state; } }; // 创建 Context interface TodoContextValue { todos: TodoItem[]; dispatch: React.DispatchTodoAction; } const TodoContext createContextTodoContextValue | undefined(undefined); // 创建 Provider 组件 export const TodoProvider ({ children }: { children: ReactNode }) { const [todos, dispatch] useReducer(todoReducer, []); // 初始为空数组 return ( TodoContext.Provider value{{ todos, dispatch }} {children} /TodoContext.Provider ); }; // 自定义 Hook方便在任何子组件中获取 Context export const useTodoContext () { const context useContext(TodoContext); if (!context) { throw new Error(useTodoContext must be used within a TodoProvider); } return context; };步骤2使用 Provider 包裹应用在src/main.tsx或App.tsx外层包裹TodoProvider使整个应用都能访问到 Context。// src/main.tsx import React from react; import ReactDOM from react-dom/client; import App from ./App; import { TodoProvider } from ./contexts/TodoContext; import ./index.css; ReactDOM.createRoot(document.getElementById(root)!).render( React.StrictMode TodoProvider {/* 在这里提供全局状态 */} App / /TodoProvider /React.StrictMode, );步骤3在深层子组件中使用 Context现在任何深度的组件如TodoDeleteButton都可以直接使用useTodoContext来派发动作无需通过 props 层层传递。// src/components/TodoDeleteButton.tsx import { useTodoContext } from ../contexts/TodoContext; import styles from ./TodoDeleteButton.module.css; interface TodoDeleteButtonProps { todoId: string; } const TodoDeleteButton ({ todoId }: TodoDeleteButtonProps) { const { dispatch } useTodoContext(); // 直接获取 dispatch 函数 const handleDelete () { // 直接派发删除动作 dispatch({ type: DELETE_TODO, payload: todoId }); }; return ( button onClick{handleDelete} className{styles.deleteButton} aria-label删除任务 × /button ); }; export default TodoDeleteButton;通信要点解耦与简化深层组件不再需要接收层层传递的回调函数直接通过 Context 消费和修改全局状态。逻辑集中所有状态更新逻辑都集中在reducer函数中使得业务逻辑更清晰、更易于测试。性能考量Context 的 value 变化会导致所有消费该 Context 的组件重新渲染。对于频繁更新的状态需要结合useMemo或memo进行优化或者考虑更专业的状态管理库如 Zustand, Redux Toolkit。4.4 React 19 新特性实战useOptimistic 与 ActionsReact 19 引入了两个与组件通信和状态管理密切相关的新 HookuseOptimistic和useActionState原名useFormState。它们特别适合处理异步操作如网络请求时的 UI 反馈。场景当用户标记一个任务为完成时我们模拟一个网络请求。在请求发起到成功返回之前我们希望 UI 能立即响应用户操作乐观更新如果请求失败再回滚状态。传统方式我们需要手动管理“正在更新”的状态和错误处理逻辑分散。使用 useOptimistic它可以让你在异步操作进行时立即显示一个预期的 UI 状态。首先我们修改App.tsx移除本地的useState改用从TodoContext获取状态并引入useOptimistic。// src/App.tsx (修改后) import { useOptimistic } from react; // 引入 React 19 新 Hook import { useTodoContext } from ./contexts/TodoContext; import { TodoItem, FilterType } from ./types/todo; import TodoInput from ./components/TodoInput; import TodoFilter from ./components/TodoFilter; import TodoList from ./components/TodoList; import ./App.css; function App() { const { todos, dispatch } useTodoContext(); const [filter, setFilter] useStateFilterType(all); // 使用 useOptimistic 管理乐观状态 const [optimisticTodos, setOptimisticTodos] useOptimistic( todos, (state, updatedTodo: TodoItem) { // 当调用 setOptimisticTodos 时立即用新的 todo 替换旧的 return state.map(t (t.id updatedTodo.id ? updatedTodo : t)); } ); // 模拟一个异步的“切换完成状态”操作 const toggleTodoWithOptimistic async (id: string) { const todoToUpdate todos.find(t t.id id); if (!todoToUpdate) return; // 1. 立即创建乐观更新后的状态 const optimisticTodo { ...todoToUpdate, completed: !todoToUpdate.completed }; // 2. 触发乐观更新UI 会立即变化 setOptimisticTodos(optimisticTodo); try { // 3. 执行实际的异步操作模拟网络请求 await new Promise(resolve setTimeout(resolve, 500)); // 模拟延迟 // 4. 异步操作成功通过 Context 的 dispatch 正式更新状态 dispatch({ type: TOGGLE_TODO, payload: id }); // 注意dispatch 会更新 todos而 optimisticTodos 会自动与最新的 todos 同步。 } catch (error) { // 5. 如果异步操作失败useOptimistic 会自动回滚到 todos 的真实状态 console.error(Failed to toggle todo:, error); // 可以在这里添加错误提示 Toast } }; const filteredTodos optimisticTodos.filter(todo { // 使用乐观状态进行过滤 if (filter active) return !todo.completed; if (filter completed) return todo.completed; return true; }); return ( div classNameapp-container h1React 19 Todo List (with Optimistic Updates)/h1 TodoInput onAddTodo{(text) dispatch({ type: ADD_TODO, payload: text })} / TodoFilter currentFilter{filter} onFilterChange{setFilter} / {/* 将乐观更新函数传递给子组件 */} TodoList todos{filteredTodos} onToggleTodo{toggleTodoWithOptimistic} / /div ); }然后修改TodoItem和TodoCheckbox组件接收并使用这个新的onToggleTodo函数。通信要点提升用户体验useOptimistic消除了异步操作带来的界面卡顿感让应用感觉更迅捷。简化状态管理它封装了“临时状态-提交-回滚”的通用模式开发者无需手动管理中间状态和错误恢复。与现有模式融合它可以很好地与 Context、Reducer 等现有模式结合是处理异步交互的利器。5. 核心组件实现与样式整合5.1 TodoList 与 TodoItem 组件实现TodoList主要负责接收过滤后的列表并渲染TodoItem则负责单条任务的展示和事件代理。// src/components/TodoList.tsx import { TodoItem } from ../types/todo; import TodoItemComponent from ./TodoItem; import styles from ./TodoList.module.css; interface TodoListProps { todos: TodoItem[]; onToggleTodo: (id: string) void; // 接收切换状态的函数 } const TodoList ({ todos, onToggleTodo }: TodoListProps) { if (todos.length 0) { return p className{styles.emptyHint}暂无任务添加一个吧/p; } return ( ul className{styles.list} {todos.map(todo ( TodoItemComponent key{todo.id} // 务必使用稳定的唯一ID作为key todo{todo} onToggle{onToggleTodo} / ))} /ul ); }; export default TodoList;// src/components/TodoItem.tsx import { TodoItem as TodoItemType } from ../types/todo; import TodoCheckbox from ./TodoCheckbox; import TodoText from ./TodoText; import TodoDeleteButton from ./TodoDeleteButton; import styles from ./TodoItem.module.css; interface TodoItemProps { todo: TodoItemType; onToggle: (id: string) void; } const TodoItem ({ todo, onToggle }: TodoItemProps) { return ( li className{${styles.item} ${todo.completed ? styles.completed : }} TodoCheckbox checked{todo.completed} onChange{() onToggle(todo.id)} // 将事件传递给父组件 / TodoText text{todo.text} isCompleted{todo.completed} todoId{todo.id} / TodoDeleteButton todoId{todo.id} / /li ); }; export default TodoItem;5.2 使用 CSS Modules 添加样式为了让组件样式独立且可维护我们使用 CSS Modules。为每个组件创建一个同名的.module.css文件。例如src/components/TodoItem.module.css.item { display: flex; align-items: center; padding: 12px 16px; margin-bottom: 8px; background-color: white; border-radius: 8px; border: 1px solid #e9ecef; transition: all 0.2s ease; } .item:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); transform: translateY(-1px); } .item.completed { opacity: 0.7; background-color: #f8f9fa; } /* 为已完成项目的文本添加删除线可以在 TodoText 组件内实现更佳 */通过这种方式每个组件的样式都封装在自己的模块中类名在构建后会被哈希化避免了全局样式污染。6. 常见问题、性能优化与排查技巧6.1 为什么列表渲染要用 key且不能用 index在TodoList组件中我们使用todo.id作为key属性。这是一个必须遵守的最佳实践。{todos.map(todo ( TodoItemComponent key{todo.id} todo{todo} ... / ))}错误做法key{index}。当列表项顺序发生变化如添加、删除、排序时React 会错误地复用 DOM 元素导致状态错乱、性能下降和不可预测的 UI 行为。正确做法使用列表中每一项稳定、唯一的标识符如id。这能帮助 React 准确识别哪些项是新增、移动或删除的从而高效地更新 DOM。踩坑实录我曾在一个项目中因为使用index作为key导致勾选复选框后状态错误地“跳转”到了其他项上。排查了很久才发现是key的问题。自那以后我养成了为任何列表数据模型首先设计id字段的习惯。6.2 如何避免不必要的组件重渲染React 的重新渲染是高效的但不当的 props 传递或 Context 使用可能导致过多渲染。使用React.memo包裹子组件对于像TodoItem这样的纯展示组件如果其 props 没有变化则不需要重新渲染。// TodoItem.tsx import React from react; // ... 组件定义 export default React.memo(TodoItem); // 使用 memo 进行记忆化React.memo会对 props 进行浅比较只有当todo和onToggle发生变化时才会重新渲染该组件。稳定回调函数引用如果父组件传递给子组件的回调函数在每次渲染时都创建新的实例如onToggle{() handleToggle(todo.id)}即使函数逻辑没变React.memo也会认为 props 变了。解决方案是使用useCallbackHook。// 在 App 或父组件中 const handleToggle useCallback((id: string) { // ... 处理逻辑 }, []); // 依赖项数组为空表示该函数是稳定的优化 Context如果 Context 中的值如{ todos, dispatch }在每次渲染时都是新的对象所有消费组件都会重新渲染。可以使用useMemo来记忆化 Context value。// 在 TodoProvider 中 const value useMemo(() ({ todos, dispatch }), [todos]); // 只有当 todos 变化时value 才更新 return TodoContext.Provider value{value}.../TodoContext.Provider;6.3 状态管理方案如何选型我们项目中演示了三种模式Props 回调适用于简单、层级浅的组件通信。Context useReducer适用于中型应用状态逻辑相对复杂需要跨多层组件共享。状态管理库如 Zustand, Redux Toolkit适用于大型、复杂应用有大量的全局状态、异步逻辑和中间件需求。选择建议从最简单的 Props 传递开始。当感到“prop drilling”痛苦时引入 Context。如果 Context 导致性能问题或逻辑过于复杂再考虑 Zustand 这类轻量级状态库。不要一开始就上 Redux除非你确定需要它强大的生态和开发工具。6.4 异步操作错误处理与用户体验在我们的useOptimistic示例中有一个try...catch块。在实际项目中错误处理至关重要。UI 反馈异步操作失败时除了控制台打印应该给用户直观的反馈。可以使用 Toast 通知组件。// 假设有一个 useToast hook const { showError } useToast(); try { // ... 异步操作 } catch (error) { showError(操作失败请重试); // useOptimistic 会自动回滚 UI无需手动操作 }重试机制对于重要的操作如保存可以提供重试按钮。加载状态对于较长的异步操作除了乐观更新还可以在按钮或项目上显示加载指示器如 spinner防止用户重复点击。通过这个从零到一的 Todo List 项目实战我们不仅实现了一个功能完整的应用更系统地演练了 React 中组件通信的三大核心模式并探索了 React 19 新特性如何优化异步交互体验。理解数据流是掌握 React 的关键希望这个项目能成为你理解这一切的坚实基石。记住最好的学习方式就是动手实践并尝试在此基础上添加新功能如任务拖拽排序、标签分类、暗色模式等继续深化你的理解。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进