Loading... *阅读时间约 10 分钟。* --- 在《[React开发者必备技能:掌握useReducer的基础和应用](https://mp.weixin.qq.com/s?__biz=MzIzMDM4MzE3Mw==&mid=2247485461&idx=1&sn=b53ddd748b3d3eca8750218b75e46582&chksm=e8b5033fdfc28a29c22880e6bb669fc41cbc1cb9c240067260e0f15ee4f544673922ad19d00b#rd)》一文,我们讨论了 React 中关于 useReducer 的基础知识。在本篇文章中,我们将讨论 useReducer 的高级技巧。本文将通过一个较为复杂“任务控制组件”——TODO 类应用中任务处理的核心操作组件,来为你演示如何在 React 程序中灵活运用 useReducer 管理组件内及组件间的复杂状态。 ## 实现思路 在 TODO 类应用中,最核心的操作是“任务”的各种处理逻辑,包括“添加任务”、“删除任务”、“过滤任务”、“搜索任务”等操作。为了便于理解,我们只实现其中的“添加任务”和“删除任务”逻辑。 同时,为了方便我们后期维护和扩展“任务”操作的功能和逻辑,我们应该将其封装成一个独立的组件。而且,组件的“添加任务”和“删除任务”两个功能还应该暴露出去以供父组件调用。 ## 子组件 `TaskControl` 这个子组件应包含以下功能: 1. 在组件中使用 `useReducer` 管理任务列表,并使用 `useRef` 创建一个引用,用于获取输入框的值。 2. 使用 `forwardRef` 包装组件,以便可以从父组件中调用该组件中“添加任务”和“删除任务”的方法。 3. 定义“添加任务”和“删除任务”的函数,用于更新任务列表。 4. 使用 `useImperativeHandle` 将“添加任务”和“删除任务”的函数暴露给父组件以供调用。 下面就让我们来看一下具体的代码实现吧: ```jsx import React, { useReducer, useRef, forwardRef, useImperativeHandle, } from "react"; // 定义 reducer 函数用于更新任务列表 function reducer(state, action) { switch (action.type) { // 添加任务 case "ADD_TASK": return [...state, action.payload]; // 删除任务 case "DELETE_TASK": const newTasks = state.filter( (task) => task.id !== Number(action.payload.id) ); return newTasks; // 如果 action.type 不支持,抛出错误 default: throw new Error(`Unsupported action type: ${action.type}`); } } // 使用 forwardRef 包装 TaskControl 组件 const TaskControl = forwardRef((props, ref) => { // 使用 useReducer 管理任务列表 const [tasks, dispatch] = useReducer(reducer, []); // 创建 inputRef 引用用于获取输入框的值 const inputRef = useRef(null); // 创建 idInputRef 引用用于获取id输入框的值 const idInputRef = useRef(null); // 添加任务的函数 function addTask() { // 从 input 中获取任务名字 const name = inputRef.current.value.trim(); if (name === "") return; // 将新的任务添加到任务列表中 dispatch({ type: "ADD_TASK", payload: { name, id: Date.now(), completed: false, }, }); // 清空 input 中的内容 inputRef.current.value = ""; // 将焦点重新聚焦到 input 元素上 inputRef.current.focus(); } // 删除任务的函数 function deleteTask() { // 根据任务的 ID 删除任务 dispatch({ type: "DELETE_TASK", payload: { id: idInputRef.current.value.trim() }, }); } // 使用 useImperativeHandle 将 addTask 和 deleteTask 函数暴露给父组件 useImperativeHandle(ref, () => { return { addTask, deleteTask }; }); // 渲染组件 return ( <div> {/* 输入框 */} <input type="text" ref={inputRef} placeholder="Enter task name" /> <input type="text" ref={idInputRef} placeholder="Enter task id" /> {/* 任务列表 */} <ul> {tasks.map((task) => ( <li key={task.id}> {task.name} (id: {task.id}) </li> ))} </ul> </div> ); }); export default TaskControl; ``` 上面的代码中,我们代码定义了一个 `reducer` 函数,用于管理任务列表的状态。这个函数接收两个参数:当前的状态和一个 action 对象。reducer 函数将根据 action 类型来更新状态。如果 action 类型不支持,则会抛出一个错误。 在这个 reducer 函数中,有三种可能的 action 类型: 1. `ADD_TASK`:添加一个任务到任务列表中。 2. `DELETE_TASK`:从任务列表中删除一个任务。 3. 其他类型:抛出一个错误,表示不支持这种类型的 action。 然后,我们使用 `forwardRef` 函数来包装组件。`forwardRef` 函数可以将一个组件转换为另一个组件,以便可以从父组件中调用该组件的方法。这个函数接收一个函数作为参数,并返回一个新的组件。 整个 `TaskControl` 组件中包含以下状态和方法: * `tasks`:任务列表的状态,使用 `useReducer` 管理。 * `inputRef`:输入框的引用,使用 `useRef` 创建。 * `addTask`:添加任务的方法,将新的任务添加到任务列表中。 * `deleteTask`:删除任务的方法,根据任务的 ID 从任务列表中删除任务。 之后,我们通过 `useImperativeHandle` 将组件的 `addTask` 和 `deleteTask` 方法暴露出去,当父组件调用这两个方法时便能实现新增任务和删除任务的操作。 接下来我们在父组件中渲染这个组件并调用它暴露出来的两个方法。 ## 父组件 `Parent` ```jsx import React, { useRef } from "react"; import TaskControl from "../components/TaskControl"; export default function Todo(): JSX.Element { // 创建一个 ref 对象来引用 TaskControl 组件的实例 const taskControlRef = useRef(null); // 点击“Add Task”按钮的事件处理函数 const handleAddTask = () => { // 通过 ref 对象调用 TaskControl 实例上的 addTask 方法 taskControlRef?.current.addTask(); }; // 点击“Delete Task”按钮的事件处理函数 const handleDeleteTask = () => { // 通过 ref 对象调用 TaskControl 实例上的 deleteTask 方法 taskControlRef.current.deleteTask(); }; return ( <div> {/* 将 taskControlRef 作为 ref 属性传递给 TaskControl 组件 */} <TaskControl ref={taskControlRef} /> <button onClick={handleAddTask}>Add Task</button> <button onClick={handleDeleteTask}>Delete Task</button> </div> ); } ``` 在上面的代码中,我们通过 ref 来调用子组件 TaskControl 中暴露出来的方法。当我们点击了页面上的“Add Task”和“Delete Task”按钮后,会通过调用子组件中的方法来实现任务列表的添加和删除操作。 具体效果如下:  ## 总结 在 TaskControl 组件中,我们通过 useReducer 来控制组件内的任务状态。在 dispatch 函数中,我们可以通过传入额外的参数给 payload 可以让每个小的 reducer 函数根据入参来丰富他们的逻辑。 灵活使用 useReducer 能让我们组织出结构明晰的代码逻辑,还能提高我们代码的可维护性和可读性。因此,除了使用 Redux 来管理组件状态之外,熟练掌握 useReducer 对我们来说也是必须的。 以上,希望对你所有帮助。  Last modification:April 19, 2023 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 1 如果觉得我的文章对你有用,请随意赞赏