前端请求的第N种方式——玩转React Hook

我曾在几年前写过一篇文章——《Jquery ajax, Axios, Fetch区别之我见》——从原理和使用层面分析了ajax,axios和fetch的区别。现在,本文从一个小的例子出发,通过使用react hook,给大家剖析一种新的数据请求方式;并通过这个自定义HOOK,引出&介绍其他的React Hook库。废话不多说,我们马上开始。

1. 故事起源

最近在用umi写一个项目,然后发现umi的网络请求竟然是这么写的:

import { useRequest } from '@umijs/hooks';
import Mock from 'mockjs';
import React from 'react';

function getUsername(): Promise<string> {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(Mock.mock('@name'));
    }, 1000);
  });
}

export default () => {
  const { data, error, loading } = useRequest(getUsername)
  if (error) {
    return <div>failed to load</div>
  }
  if (loading) {
    return <div>loading...</div>
  }
  return <div>Username: {data}</div>
}

PS:试一下

在这个例子中,我们使用useRequest对异步请求getUsername进行封装,最终获得了一个对象,其中包含三个值:

data: 正常请求返回的数据
error: 异常请求的错误信息
loading: 是否在加载/请求的状态

这就让我们省却了将新请求的数据设置为组件的状态等逻辑,让整个代码变得清晰明了了很多。而如果不使用useRequest,我们代码应该是下面这样:

import { useRequest } from "@umijs/hooks";
import Mock from "mockjs";
import React from "react";

const { useState, useEffect } = React;

function getUsername(): Promise<string> {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(Mock.mock("@name"));
    }, 1000);
  });
}

export default () => {
  const [username, setUsername] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  // 异步请求还是要封装一下
  async function getUsernameAsync() {
    setLoading(true);
    const res = await getUsername();
    // 在本例子中,getUsername永远返回的都是请求正确的值,我们这边假设当请求失败时,它会返回一个error对象
    if (typeof res === "string") {
      setUsername(res);
    } else {
      setError(res);
    }
    setLoading(false);
  }
  // 发起请求
  useEffect(() => {
    getUsernameAsync();
  }, []);

  if (error) {
    return <div>failed to load</div>;
  }
  if (loading) {
    return <div>loading...</div>;
  }
  return <div>Username: {username}</div>;
};

这下就应该很明确了,useRequest帮我们抹平了三个state值,让我们可以直接去使用它们。这就有点神奇了,它是怎么实现的呢?是否还有什么其他的能力?马上进入下一趴!

2. 实现原理

在本节,我们通过对react-use的useAsync进行代码分析,来解释上一节中为什么@umijs/hooks的useRequest可以那么做

useAsync代码相对简单,useRequest的代码中掺杂了很多它的其他特性,本质上它们的原理是一致的。
react-use: [项目地址]一个react hook函数库,相当于react hook的JQuery,提供多种基本的基础Hook封装能力:如发送请求,获取/设置cookie,获取鼠标的位置等等。

PS:react-use的中文翻译版本还是针对V8.1.3的,而现在的版本是V15.1.1,所以可以直接看英文文档。

首先还是先贴一下使用方式:

import {useAsync} from 'react-use';

function getUsername(): Promise<string> {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(Mock.mock("@name"));
    }, 1000);
  });
}

const Demo = () => {
  const state = useAsync(getUsername);

  return (
    <div>
      {state.loading?
        <div>Loading...</div>
        : state.error?
        <div>Error...</div>
        : <div>Value: {state.value}</div>
      }
    </div>
  );
};

可以看到,使用方法都是一样的,那么下面我们来看下它们是如何实现的——因为useAsync引用了自身的useAsyncFn,所以我们下面直接来分析useAsyncFn。

/* eslint-disable */
// import暂时都不关心
import { DependencyList, useCallback, useState, useRef } from 'react';
import useMountedState from './useMountedState';
import { FnReturningPromise, PromiseType } from './util';
// AsyncState有四种状态
export type AsyncState<T> =
  | {
      loading: boolean;
      error?: undefined;
      value?: undefined;
    }
  | {
      loading: true;
      error?: Error | undefined;
      value?: T;
    }
  | {
      loading: false;
      error: Error;
      value?: undefined;
    }
  | {
      loading: false;
      error?: undefined;
      value: T;
    };
// ts的类型暂时也都不关心
type StateFromFnReturningPromise<T extends FnReturningPromise> = AsyncState<PromiseType<ReturnType<T>>>;
export type AsyncFnReturn<T extends FnReturningPromise = FnReturningPromise> = [StateFromFnReturningPromise<T>, T];
// 正文,接受三个参数,一个异步请求函数,一个数组类型的依赖,一个初始状态,默认是loading=false
export default function useAsyncFn<T extends FnReturningPromise>(
  fn: T,
  deps: DependencyList = [],
  initialState: StateFromFnReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
  // 记录是第几次调用
  const lastCallId = useRef(0);
  // 判断当前组件是否mounted完成
  const isMounted = useMountedState();
  // 设置状态
  const [state, set] = useState<StateFromFnReturningPromise<T>>(initialState);
  // useCallback就是用来性能优化的,保证返回同一个回调函数,因此,我们直接看它内部的回调函数就行
  const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
    // 第几次调用
    const callId = ++lastCallId.current;
    // 直接设置为loading为true
    set(prevState => ({ ...prevState, loading: true }));
    // 当异步请求结束时,设置状态
    return fn(...args).then(
      value => {
        // 当用户多次请求时,只返回最后一次请求的值
        isMounted() && callId === lastCallId.current && set({ value, loading: false });

        return value;
      },
      error => {
        isMounted() && callId === lastCallId.current && set({ error, loading: false });

        return error;
      }
    ) as ReturnType<T>;
  }, deps);
  // 返回两个值,一个是state,一个是封装好的callback
  return [state, (callback as unknown) as T];
}

这就是useAsyncFn的实现逻辑,可以看到本质上和我们第一节上的疯狂设置状态原理也差不多,但是这样对请求的封装却打开了新的大门,具体有哪些能力呢?

3. SWR和useRequest介绍

毫无疑问,第一节和第二节对异步请求的封装避免了我们在不同的请求中反复的处理状态,是能大幅提效的。那是否还有更牛逼的能力呢?它们来了!

SWR:[项目地址]一个React hook的异步请求库。提供了异步请求的多种能力,例如:swr(stale-while-revalidate,先返回之前请求的缓存数据,再重新请求并刷新)能力,分页,屏幕聚焦发送请求等。useRequest也是借鉴了SWR的思路。
useRequest: [项目地址]蚂蚁中台标准请求Hook仓库。提供了多种能力,并被内置到umi中。

image.png

具体的能力大家可以看《useRequest- 蚂蚁中台标准请求 Hooks》,已经很详尽了,我就不过多介绍了。

4. 总结

本文从一个umi request的使用case入手,分析了它的原理。并在这个过程中,分别介绍了react-use这个通用基础hook仓库,和SWR/useRequest这两个异步请求hook仓库。通过使用它们的能力,可以大幅提升我们的代码编写效率。

如果你还没有使用react hook的话,请马上加入到react hook的大家庭中,因为随着它的发展,它真的拥有了很多class component所不具备的特性。

阅读 1.1k

推荐阅读
汤姆C
用户专栏

tomczhang的专栏,我会在这个专栏中撰写我的思考,工作中遇到的问题和苦恼,每周一篇

312 人关注
43 篇文章
专栏主页