封装通用逻辑useAsync

封装通用逻辑useAsync

Content #

在组件的开发过程中,有一些常用的通用逻辑。过去可能会因为逻辑重用比较繁琐,而经常在每个组件中去自己实现,造成维护的困难。但现在有了 Hooks,就可以将更多的通用逻辑通过 Hooks 的形式进行封装,方便被不同的组件重用。

比如说,在日常 UI 的开发中,有一个最常见的需求:发起异步请求获取数据并显示在界面上。在这个过程中,我们不仅要关心请求正确返回时,UI 会如何展现数据;还需要处理请求出错,以及关注 Loading 状态在 UI 上如何显示。

我们可以看下异步请求的例子,从 Server 端获取用户列表,并显示在界面上:

import React from "react";

export default function UserList() {
  // 使用三个 state 分别保存用户列表,loading 状态和错误状态
  const [users, setUsers] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  // 定义获取用户的回调函数
  const fetchUsers = async () => {
    setLoading(true);
    try {
      const res = await fetch("https://reqres.in/api/users/");
      const json = await res.json();
      // 请求成功后将用户数据放入 state
      setUsers(json.data);
    } catch (err) {
      // 请求失败将错误状态放入 state
      setError(err);
    }
    setLoading(false);
  };

  return (
    <div className="user-list">
      <button onClick={fetchUsers} disabled={loading}>
        {loading ? "Loading..." : "Show Users"}
      </button>
      {error &&
        <div style={{ color: "red" }}>Failed: {String(error)}</div>
      }
      <br />
      <ul>
        {users && users.length > 0 &&
          users.map((user) => {
            return <li key={user.id}>{user.first_name}</li>;
          })}
      </ul>
    </div>
  );
}

在这里,我们定义了 users、loading 和 error 三个状态。如果我们在异步请求的不同阶段去设置不同的状态,这样 UI 最终能够根据这些状态展现出来。在每个需要异步请求的组件中,其实都需要重复相同的逻辑。

事实上,在处理这类请求的时候,模式都是类似的,通常都会遵循下面步骤:

  1. 创建 data,loading,error 这 3 个 state;
  2. 请求发出后,设置 loading state 为 true;
  3. 请求成功后,将返回的数据放到某个 state 中,并将 loading state 设为 false;
  4. 请求失败后,设置 error state 为 true,并将 loading state 设为 false。

最后,基于 data、loading、error 这 3 个 state 的数据,UI 就可以正确地显示数据,或者 loading、error 这些反馈给用户了。

所以,通过创建一个自定义 Hook,可以很好地将这样的逻辑提取出来,成为一个可重用的模块。比如代码可以这样实现:

import { useState } from 'react';

const useAsync = (asyncFunction) => {
  // 设置三个异步逻辑相关的 state
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  // 定义一个 callback 用于执行异步逻辑
  const execute = useCallback(() => {
    // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
    setLoading(true);
    setData(null);
    setError(null);
    return asyncFunction()
      .then((response) => {
        // 请求成功时,将数据写进 state,设置 loading 为 false
        setData(response);
        setLoading(false);
      })
      .catch((error) => {
        // 请求失败时,设置 loading 为 false,并设置错误状态
        setError(error);
        setLoading(false);
      });
  }, [asyncFunction]);

  return { execute, loading, data, error };
};

那么有了这个 Hook,我们在组件中就只需要关心与业务逻辑相关的部分。比如代码可以简化成这样的形式:

import React from "react";
import useAsync from './useAsync';

export default function UserList() {
  // 通过 useAsync 这个函数,只需要提供异步逻辑的实现
  const {
    execute: fetchUsers,
    data: users,
    loading,
    error,
  } = useAsync(async () => {
    const res = await fetch("https://reqres.in/api/users/");
    const json = await res.json();
    return json.data;
  });

  return (
    // 根据状态渲染 UI...
  );
}

通过这个例子可以看到,我们利用了 Hooks 能够管理 React 组件状态的能力,将一个组件中的某一部分状态独立出来,从而实现了通用逻辑的重用。

不过在这里你可能会有一个疑问:这种类型的封装我写一个工具类不就可以了?为什么一定要通过 Hooks 进行封装呢?

答案很容易就能想到。因为在 Hooks 中,你可以管理当前组件的 state,从而将更多的逻辑写在可重用的 Hooks 中。但是要知道,在普通的工具类中是无法直接修改组件 state 的,那么也就无法在数据改变的时候触发组件的重新渲染。

Viewpoints #

From #

06|自定义Hooks :四个典型的使用场景