阅读时间约 10 分钟。
在《React开发者必备技能:掌握useReducer的基础和应用》一文,我们讨论了 React 中关于 useReducer 的基础知识。在本篇文章中,我们将讨论 useReducer 的高级技巧。本文将通过一个较为复杂“任务控制组件”——TODO 类应用中任务处理的核心操作组件,来为你演示如何在 React 程序中灵活运用 useReducer 管理组件内及组件间的复杂状态。
实现思路
在 TODO 类应用中,最核心的操作是“任务”的各种处理逻辑,包括“添加任务”、“删除任务”、“过滤任务”、“搜索任务”等操作。为了便于理解,我们只实现其中的“添加任务”和“删除任务”逻辑。
同时,为了方便我们后期维护和扩展“任务”操作的功能和逻辑,我们应该将其封装成一个独立的组件。而且,组件的“添加任务”和“删除任务”两个功能还应该暴露出去以供父组件调用。
子组件 TaskControl
这个子组件应包含以下功能:
- 在组件中使用
useReducer
管理任务列表,并使用useRef
创建一个引用,用于获取输入框的值。 - 使用
forwardRef
包装组件,以便可以从父组件中调用该组件中“添加任务”和“删除任务”的方法。 - 定义“添加任务”和“删除任务”的函数,用于更新任务列表。
- 使用
useImperativeHandle
将“添加任务”和“删除任务”的函数暴露给父组件以供调用。
下面就让我们来看一下具体的代码实现吧:
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 类型:
ADD_TASK
:添加一个任务到任务列表中。DELETE_TASK
:从任务列表中删除一个任务。- 其他类型:抛出一个错误,表示不支持这种类型的 action。
然后,我们使用 forwardRef
函数来包装组件。forwardRef
函数可以将一个组件转换为另一个组件,以便可以从父组件中调用该组件的方法。这个函数接收一个函数作为参数,并返回一个新的组件。
整个 TaskControl
组件中包含以下状态和方法:
tasks
:任务列表的状态,使用useReducer
管理。inputRef
:输入框的引用,使用useRef
创建。addTask
:添加任务的方法,将新的任务添加到任务列表中。deleteTask
:删除任务的方法,根据任务的 ID 从任务列表中删除任务。
之后,我们通过 useImperativeHandle
将组件的 addTask
和 deleteTask
方法暴露出去,当父组件调用这两个方法时便能实现新增任务和删除任务的操作。
接下来我们在父组件中渲染这个组件并调用它暴露出来的两个方法。
父组件 Parent
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 对我们来说也是必须的。
以上,希望对你所有帮助。
One comment
叼茂SEO.bfbikes.com