阅读时间约 10 分钟。


在《React开发者必备技能:掌握useReducer的基础和应用》一文,我们讨论了 React 中关于 useReducer 的基础知识。在本篇文章中,我们将讨论 useReducer 的高级技巧。本文将通过一个较为复杂“任务控制组件”——TODO 类应用中任务处理的核心操作组件,来为你演示如何在 React 程序中灵活运用 useReducer 管理组件内及组件间的复杂状态。

实现思路

在 TODO 类应用中,最核心的操作是“任务”的各种处理逻辑,包括“添加任务”、“删除任务”、“过滤任务”、“搜索任务”等操作。为了便于理解,我们只实现其中的“添加任务”和“删除任务”逻辑。

同时,为了方便我们后期维护和扩展“任务”操作的功能和逻辑,我们应该将其封装成一个独立的组件。而且,组件的“添加任务”和“删除任务”两个功能还应该暴露出去以供父组件调用。

子组件 TaskControl

这个子组件应包含以下功能:

  1. 在组件中使用 useReducer 管理任务列表,并使用 useRef 创建一个引用,用于获取输入框的值。
  2. 使用 forwardRef 包装组件,以便可以从父组件中调用该组件中“添加任务”和“删除任务”的方法。
  3. 定义“添加任务”和“删除任务”的函数,用于更新任务列表。
  4. 使用 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 类型:

  1. ADD_TASK:添加一个任务到任务列表中。
  2. DELETE_TASK:从任务列表中删除一个任务。
  3. 其他类型:抛出一个错误,表示不支持这种类型的 action。

然后,我们使用 forwardRef 函数来包装组件。forwardRef 函数可以将一个组件转换为另一个组件,以便可以从父组件中调用该组件的方法。这个函数接收一个函数作为参数,并返回一个新的组件。

整个 TaskControl 组件中包含以下状态和方法:

  • tasks:任务列表的状态,使用 useReducer 管理。
  • inputRef:输入框的引用,使用 useRef 创建。
  • addTask:添加任务的方法,将新的任务添加到任务列表中。
  • deleteTask:删除任务的方法,根据任务的 ID 从任务列表中删除任务。

之后,我们通过 useImperativeHandle 将组件的 addTaskdeleteTask 方法暴露出去,当父组件调用这两个方法时便能实现新增任务和删除任务的操作。

接下来我们在父组件中渲染这个组件并调用它暴露出来的两个方法。

父组件 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”按钮后,会通过调用子组件中的方法来实现任务列表的添加和删除操作。

具体效果如下:

任务控制组件演示.gif

总结

在 TaskControl 组件中,我们通过 useReducer 来控制组件内的任务状态。在 dispatch 函数中,我们可以通过传入额外的参数给 payload 可以让每个小的 reducer 函数根据入参来丰富他们的逻辑。

灵活使用 useReducer 能让我们组织出结构明晰的代码逻辑,还能提高我们代码的可维护性和可读性。因此,除了使用 Redux 来管理组件状态之外,熟练掌握 useReducer 对我们来说也是必须的。

以上,希望对你所有帮助。

关注领福利

Last modification:August 15, 2024
如果觉得我的文章对你有用,请随意赞赏