React 中发送请求的方式

2022/8/7 reactfetch

一个完整项目,通常都需要跟服务器进行异步的数据交互。在 React 的项目实践中,发现数据请求经历过一系列有意思的变化。

# useEffect

最开始在 React 开发的项目中,如果有组件加载完成就要发送请求获取数据的这种场景,一般都是借助于 useEffectdeps[] 相当于 mount 的特性从而可以在其中发送请求,再把返回的数据 setState 从而完整获取数据整个流程。

以从 GitHub 中获取用户列表后显示获取到第一个用户的名称为例:

import { useEffect, useState } from "react";

export const App = () => {
  const [users, setUsers] = useState<{ login: string }[]>([]);

  useEffect(() => {
    fetch("https://api.github.com/users?since=10").then(async (res) => {
      setUsers(await res.json());
    });
  }, []);

  return <div>{users[0]?.login}</div>;
};
1
2
3
4
5
6
7
8
9
10
11
12
13

上述过程看起来是完成了一次发送请求获取数据的流程,但是如果实际使用会有一些问题,比如请求状态、是否出错等情况的处理,为此可能要再加上 error 和 loading 的 state,然后再进行处理。

# useAsync

后面发现了 react-use 的 useAsync (opens new window) 这个用于处理 async 函数的 hook,内部做了各种处理后返回一个包含 loading、error 和 value 的 state,非常符合对处理请求的预期。

import { useAsync } from "react-use";

export const App = () => {
  const state = useAsync(async () => {
    const response = await fetch("https://api.github.com/users?since=10");
    const result = await response.json();
    return result as { login: string }[];
  }, []);

  return (
    <div>
      {state.loading ? (
        <div>Loading...</div>
      ) : state.error ? (
        <div>Error: {state.error.message}</div>
      ) : (
        <div>Value: {state.value?.[0]?.login}</div>
      )}
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

用了 useAsync 之后感觉处理请求就很方便了,不管是组件加载就要获取数据还是根据依赖重新请求数据都能妥当的处理好。

# useSwr

useAsync 使用上一直都还不错,直到在一个新的项目中,由于用的是 React18,就出现了之前说过的一个问题,加载组件时候发送的请求会发送两遍,具体现象和原因可以参考 React 18 useEffect 执行多次

很明显如果请求都是发送两遍是不符合我们预期的,就算有方法可以暂时避免掉这个问题也不是长久之策,我们需要一个真正 用于数据请求的 React 库。目前常见的方案有 SWR (opens new window) 以及 React-Query (opens new window) 等方案都是适用的,可以根据要针对的功能常见进行选用。

以 SWR 为例进行说明,不涉及具体使用,具体使用可以参考 中文文档 (opens new window),不管是配置项还是示例都非常详细,上手使用无难度。

使用 SWR,组件将会不断地、自动获得最新数据流。UI 也会一直保持快速响应。

import useSWR from "swr";

const fetcher = (input: RequestInfo | URL, ...args: any[]) =>
  fetch(input, ...args).then((res) => res.json());

export const App = () => {
  const { data, error } = useSWR<{ login: string }[]>(
    "https://api.github.com/users?since=10",
    fetcher
  );

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data[0]?.login}!</div>;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实现之前相同常见的例子不仅需要的代码量更少,还解决了重复请求的问题,更拥有了许多非常有意思的特性。

一般使用主要就是用 const { data, error, isValidating, mutate } = useSWR(key, fetcher, options) 这个 hook,API 选项 (opens new window) 中给出 key 是请求的唯一 的标志,同时根据这个 key 可以 条件性的请求数据 (opens new window) 或者传入 参数 (opens new window) 进行请求。

用下来发现比较好方式可能是把请求封装成数据 hooks 的形式进行使用,可以把数据处理逻辑放到一起还能方便调用。

// 定义hook
function useUser(id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error,
  }
}

// 在组件中使用
function Avatar({ id }) {
  const { user, isLoading, isError } = useUser(id)

  if (isLoading) return <Spinner />
  if (isError) return <Error />
  return <img src={user.avatar} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

SWR 内置的 缓存重复数据删除 会跳过不必要的网络请求,就是判断是不是具有相同的 SWR key,在不同的地方重复调用数据 hooks 是不会重复发送请求的。

# 参考