8
头图

入门react-query

写在前面

由于国内较少有比较系统的react-query教程,因此笔者结合官方文档以及官方课程的内容,希望写一个较为全面的教程。本文将以各种例子作为切入点,尽可能通俗易懂地讲解相关知识点。如果有错误,还请大家在评论区指出,笔者会尽快改正。

目录

版本说明

本教程基于react-query@4版本编写,此版本目前(2022-06-05)为alpha版本。

在线演示基于stackblitz 平台

从在react中后端请求数据开始说起

在日常的开发中,免不了请求后端接口。在请求接口时,经常会涉及到以下处理

  • 加载状态
  • 后端返回数据存储
  • 如果接口有报错信息,展示报错信息
  • 刷新数据
  • 等等

    下面来看一个满足了上述处理的例子
     点我查看例子①在线演示 

PS:以下代码将会从github请求一句富有禅意(逼格高)的话,并显示在页面上

例子①👇🏻

import * as React from 'react';

export default function App() {
  // 存储 后端返回数据
  const [zen, setZen] = React.useState('');
  // 存储 加载状态
  const [isLoading, setIsLoading] = React.useState(false);
  // 存储 是否请求成功
  const [isError, setIsError] = React.useState(false);
  // 存储 后端返回的错误数据
  const [errorMessage, setErrorMessage] = React.useState('');

  const fetchData = () => {
    // 开始获取数据,将isLoading置为true
    setIsLoading(true);

    fetch('https://api.github.com/zen')
      .then(async (response) => {
        // 如果请求返回status不为200 则抛出后端错误
        if (response.status !== 200) {
          const { message } = await response.json();

          throw new Error(message);
        }

        return response.text();
      })
      .then((text: string) => {
        // 请求完成将isLoading置为false
        setIsLoading(false);
        // 接口请求成功,将isError置为false
        setIsError(false);
        // 存储后端返回的数据
        setZen(text);
      })
      .catch((error) => {
        // 请求完成将isLoading置为false
        setIsLoading(false);
        // 接口请求错误,将isError置为true
        setIsError(true);
        // 存储后端返回的错误数据
        setErrorMessage(error.message);
      });
  };

  React.useEffect(() => {
    // 初始化请求数据
    fetchData();
  }, []);

  return (
    <div>
      <h1>Zen from Github</h1>
      <p>{isLoading ? '加载中...' : isError ? errorMessage : zen}</p>
      {!isLoading && (
        <button onClick={fetchData}>{isError ? '重试' : '刷新'}</button>
      )}
    </div>
  );
}

在上面的例子中

  • 使用isLoading来存储加载状态
  • 使用isError 来存储接口是否有错误
  • 使用errorMessage 来存储后端返回的报错信息
  • 使用zen来存储 后端返回数据存储 
  • 重新调用fetchData 方法来刷新数据

该例子仅仅是请求一个接口,假如是一个真实的项目,铁定不止这一个请求,因此我们将要写大量重复代码,来满足业务需求(内心os:其实是代码写的太多影响效率,不能早下班(¬_¬),而且维护起来成本高)_

此时引入react-query可以减少与请求接口相关的代码,上面的例子使用react-query重写如下:
 点我查看例子②在线演示 
例子②👇🏻

import * as React from 'react';
import { useQuery } from 'react-query';

const fetchData = () => {
  return fetch('https://api.github.com/zen').then(async (response) => {
    // 如果请求返回status不为200 则抛出后端错误
    if (response.status !== 200) {
      const { message } = await response.json();

      throw new Error(message);
    }

    return response.text();
  });
};

export default function App() {
  const zenQuery = useQuery(['zen'], fetchData); // ①

  return (
    <div>
      <h1>Zen from Github</h1>
      <p>
        {zenQuery.isLoading || zenQuery.isFetching
          ? '加载中...'
          : zenQuery.isError
          ? zenQuery.error?.message
          : data}
      </p>
      {!zenQuery.isLoading && !zenQuery.isFetching && (
        <button
          onClick={() => {
            zenQuery.refetch();
          }}
        >
          {zenQuery.isError ? '重试' : '刷新'}
        </button>
      )}
    </div>
  );
}

对比一下,在引入了react-query之后,肉眼可见代码量在降低!!

不需要写useState来管理因为请求接口带来的额外状态(如果使用react-redux、mobx等状态管理库,同样会遇到类似的问题),同样也不需要在useEffect(() => {}, [])中初始化调用接口,react-query会帮我们处理。

你可能对于上面例子中,在代码①引入useQuery钩子时,传入的参数不是很了解,接下来我们将会介绍这些传入的参数。

查询键(Query Keys)及查询函数(Query Functions)

什么是查询键(Query Keys)及查询函数(Query Functions)?

在大家日常开发的过程中,请求后端数据时:

  • 会先写一个函数来请求后端接口的数据(如上面例子①中的fetchData函数)
  • 接着指定一个变量(如上面例子①中的zen)来存储相关后端返回的数据,每个接口的变量针对不同的接口会起不同的名字,来标识不同的数据。

那么在react-query中如何区分不同的接口获取的不同数据呢?

回到例子②中,我们使用useQuery钩子来获取后端数据,代码如下:

const zenQuery = useQuery(['zen'], fetchData);
  • 其中['zen'] 就是react-query的查询键,react-query通过不同的查询键来标识(映射)不同接口(或是同一接口不同参数请求)返回的数据。在react-query@4中,查询键必须是数组。
  • fetchData就是我们请求后端接口的函数,也就是查询函数。

PS:查询键内的元素可以是嵌套数组、对象、字符串、数字

例如:['zen', { form: 'confucius' }]['zen', ['confucius', 'Lao Tzu']]

为了方便记忆,打个比方,你可以将查询键看做是你存储localStorage时的key,而value则是通过查询函数查询到数据后,将各种我们需要的状态数据存储进入value

PS:当然实际的处理过程及存储的信息会很复杂,不过思路基本上一致。

写查询键的一些小建议

解释完查询键和查询函数后,笔者希望大家考虑一个问题,这个接口比较简单,因此我们可以使用zen来作为查询键,假如我有一个复杂的接口,此时应该如何更好的设计查询键呢?

还是以github的接口为例,如果你想获取到github中某个仓库的issue列表,你可以这样调用接口

https://api.github.com/repos/{owner}/{repo}/issues

具体一点,假如希望获取react仓库issue列表可以调用下面的接口,你可以在浏览器中打开尝试一下:

https://api.github.com/repos/facebook/react/issues

此时,你可以通过请求接口,拿到react仓库内的issue列表。

以这个获取仓库issue列表接口为例,可以这样写查询键
例子③👇🏻

['issues', owner, repo]

在这个查询键中我们遵循了一个原则:从通用到特殊

首先我们获取的数据类型是issue,我们需要在数组的开头放一个字符串来标识数据类型,因此第一个参数我们设定为issues。在github中有许多仓库,这些仓库通常以用户作为第一级标识,仓库名是第二级标识,如下图所示
react仓库

因此第二个和第三个参数依次是ownerrepo

上面的例子中,我们没有使用['issues', 'facebook', 'react']而是使用['issues', owner, repo]的原因是为了介绍在react-query中,使用变量作为查询键的元素时,当变量的值变化后,react-query将会重新调用fetchData方法,获取新的数据,并缓存到对应变量值为key的缓存中。

即发生下面的变化时,react-query将会重新调用fetchData方法,并将从后端获取到的数据,缓存在查询键为['issues', 'vuejs', 'vue']对应的值中,同理我们在初始化调用接口时,获取的数据时缓存在查询键为['issues', 'facebook', 'react']的对应值中:

['issues', 'facebook', 'react'] -> ['issues', 'vuejs', 'vue'] // 从查询react仓库的issue,变更为查询vue仓库的issue

下面的例子将会获取react仓库中最新一条issue,你可以查看例子④的在线演示

将示例中输入框内的:facebook更换为vuejs,将react更换为vue,点击【查看最新issue信息】按钮,就可以看到vue仓库最新的issue信息(针对相关的数据缓存,你可以想一下上面我们说过的例子)

点我查看例子④在线演示

例子④👇🏻

import * as React from 'react';
import { useQuery } from 'react-query';

const fetchData = ({ queryKey }) => {
  const [, owner, repo] = queryKey;

  return fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
    headers: {
      Authorization: '',
    },
  }).then(async (response) => {
    // 如果请求返回status不为200 则抛出后端错误
    if (response.status !== 200) {
      const { message } = await response.json();

      throw new Error(message);
    }

    return response.json();
  });
};

export default function App() {
  const [inputOwner, setInputOwner] = React.useState('facebook');
  const [inputRepo, setInputRepo] = React.useState('react');
  const [queryKey, setQueryKey] = React.useState([inputOwner, inputRepo]);
  const issueQuery = useQuery(['issues', ...queryKey], fetchData);

  return (
    <div>
      <span>仓库:</span>
      <input
        name={'owner'}
        value={inputOwner}
        onChange={(e) => setInputOwner(e.target.value)}
      />
      /
      <input
        name={'repo'}
        value={inputRepo}
        onChange={(e) => setInputRepo(e.target.value)}
      />
      <button
        onClick={() => {
          setQueryKey([inputOwner, inputRepo]);
        }}
      >
        查看最新issue信息
      </button>
      <div>
        <h1>
          仓库{queryKey[0]}/{queryKey[1]}最新一条issue信息
        </h1>
        <p>
          {issueQuery.isLoading
            ? '加载中...'
            : issueQuery.isError
            ? issueQuery.message
            : JSON.stringify(issueQuery.data[0])}
        </p>
      </div>
    </div>
  );
}

在这个例子中,当查询键变量的值变化后,react-query将会自动请求变化后对应的数据,并且在查询函数传入的参数中,我们也可以拿到调用查询函数时查询键的值。

你可以在DevTool中,查看react-query的缓存信息,帮助你理解:

使用DevTool

缓存信息


修仙大橙子
108 声望17 粉丝

前端工程师