使用 react 的 hook 实现一个 useRequest

我们在使用 react 开发前端应用时,不可避免地要进行数据请求,而在数据请求的前后都要执行很多的处理,例如

  1. 展示 loading 效果告诉用户后台正在处理请求;
  2. 若要写 async-await,每次都要 try-catch 进行包裹;
  3. 接口异常时要进行错误处理;

当请求比较多时,每次都要重复这样的操作。这里我们可以利用 react 提供的 hook,自己来封装一个 useRequest 。

基础结构

明确 useRequest 的输入和输出。

要输入的数据:

  • url:要请求的接口地址;
  • data:请求的数据(默认只有 get 方式);
  • config:其他一些配置,可选,如(manual?: boolean; // 是否需要手动触发);

要返回的数据:

  • loading:数据是否在请求中;
  • data:接口返回的数据;
  • error:接口异常时的错误;

具体的实现

2.1 不带 config 配置的

1const useRequest = (url, data, config) => {
2  const [loading, setLoading] = useState(true);
3  const [result, setResult] = useState(null);
4  const [error, setError] = useState(null);
5
6  const request = async () => {
7    setLoading(true);
8    try {
9      const result = await axios({
10        url,
11        params: data,
12        method: "get",
13      });
14      if (result && result.status >= 200 && result.status <= 304) {
15        setResult(result.data);
16      } else {
17        setError(new Error("get data error in useRequest"));
18      }
19    } catch (reason) {
20      setError(reason);
21    }
22    setLoading(false);
23  };
24  useEffect(() => {
25    request();
26  }, []);
27
28  return {
29    loading,
30    result,
31    error,
32  };
33};

在我们不考虑第三个配置 config 的情况下,这个 useRequest 就已经可以使用了。

1const App = () => {
2  const { loading, result, error } = useRequest(url);
3
4  return (
5    <div>
6      <p>loading: {loading}</p>
7      <p>{JSON.stringify(result)}</p>
8    </div>
9  );
10};

2.2 添加取消请求

我们在请求接口过程中,可能接口还没没有返回到数据,组件就已经被销毁了,因此我们还要添加上取消请求的操作,避免操作已经不存在的组件。关于 axios 如何取消请求,您可以查看之前写的一篇文章: axios 源码系列之如何取消请求

1const request = useCallback(() => {
2  setLoading(true);
3  const CancelToken = axios.CancelToken;
4  const source = CancelToken.source();
5
6  axios({
7    url,
8    params: data,
9    method: "get",
10    cancelToken: source.token, // 将token注入到请求中
11  })
12    .then((result) => {
13      setResult(result.data);
14      setLoading(false);
15    })
16    .catch((thrown) => {
17      // 只有在非取消的请求时,才调用setError和setLoading
18      // 否则会报组件已被卸载还会调用方法的错误
19      if (!axios.isCancel(thrown)) {
20        setError(thrown);
21        setLoading(false);
22      }
23    });
24
25  return source;
26}, [url, data]);
27
28useEffect(() => {
29  const source = request();
30  return () => source.cancel("Operation canceled by the user.");
31}, [request]);

2.3 带有 config 配置的

然后,接着我们就要考虑把配置加上了,在上面的调用中,只要调用了 useRequest,就会马上触发。但若在符合某种情况下才触发怎办呢(例如用户点击后才产生接口请求)? 这里我们就需要再加上一个配置。

1{
2  "manual": false, // 是否需要手动触发,若为false则立刻产生请求,若为true
3  "ready": false // 当manual为true时生效,为true时才产生请求
4}

这里我们就要考虑 useRequest 中触发 reqeust 请求的条件了:

  • 没有 config 配置,或者有 config 且 manual 为 false;
  • 有 config 配置,且 manual 和 ready 均为 true;
1const useRequest = () => {
2  // 其他代码保持不变
3  useEffect(() => {
4    if (!config || !config.manual || (config.manual && config.ready)) {
5      const source = request();
6      return () => source.cancel("Operation canceled by the user.");
7    }
8  }, [config]);
9};

使用方法

1const App = () => {
2  const [ready, setReady] = useState(false);
3  const { loading, result, error } = useRequest(url, null, {
4    manual: true,
5    ready,
6  });
7
8  return (
9    <div>
10      <p>loading: {loading}</p>
11      <p>{JSON.stringify(result)}</p>
12      <button onClick={() => setReady(true)}>产生请求</button>
13    </div>
14  );
15};

当然,实现手动触发的方式有很多,我在这里是用一个 config.ready 来触发,在 umi 框架中,是对外返回了一个 run 函数,然后执行 run 函数再来触发这个 useRequest 的执行。

1const useRequest = () => {
2  // 其他代码保持不变,并暂时忽略
3  const [ready, setReady] = useState(false);
4
5  const run = (r: boolean) => {
6    setReady(r);
7  };
8
9  useEffect(() => {
10    if (!config || !config.manual || (config.manual && ready)) {
11      if (loading) {
12        return;
13      }
14      setLoading(true);
15      const source = request();
16
17      return () => {
18        setLoading(false);
19        setReady(false);
20        source.cancel("Operation canceled by the user.");
21      };
22    }
23  }, [config, ready]);
24};