【React Query】终极教程04:高级查询之并行查询、分页查询、无限查询

上一篇,我们对 useQuery 的使用做了非常详细的介绍,本篇将介绍一些高级的查询功能:

  • 并行查询
  • 分页查询
  • 无限查询

并行查询

开发中经常会遇到并行查询的场景。并行查询是同时执行的查询,以避免出现顺序网络请求,通常称为网络瀑布。这对于前端性能优化非常重要,因为并行查询通过同时执行所有请求来避免网络瀑布。

并行查询主要有两种方式:

  • 手动
  • 动态

手动并行查询

如果现在让你手动做并行查询,根据之前的知识,你可能就会这么做:

jsx 复制代码
import { useQuery } from "@tanstack/react-query";
import "../App.css";
import { fetchData } from "../api";
import { Table } from "antd";

function ManualParallelQueries() {
  const { data: p_data_1 } = useQuery({
    queryKey: [{ queryIdentifier: "api", q: "iPhone" }],
    queryFn: fetchData,
  });

  const { data: p_data_2 } = useQuery({
    queryKey: [{ queryIdentifier: "api", q: "OPPO" }],
    queryFn: fetchData,
  });

  const { data: p_data_3 } = useQuery({
    queryKey: [{ queryIdentifier: "api", q: "MacBook" }],
    queryFn: fetchData,
  });

  const products = [
    p_data_1?.products ?? [],
    p_data_2?.products ?? [],
    p_data_3?.products ?? [],
  ];

  const columns = [
    {
      title: "名称",
      dataIndex: "title",
      key: "title",
    },
    {
      title: "价格",
      dataIndex: "price",
      key: "price",
    },
    {
      title: "品牌",
      dataIndex: "brand",
      key: "brand",
    },
  ];

  return (
    <>
      {products.map((item) => (
        <Table dataSource={item} columns={columns} rowKey={"id"} />
      ))}
    </>
  );
}

export default ManualParallelQueries;

查看结果:

动态并行查询

虽然手动并行查询也能 cover 大多数场景,但如果你的查询次数发生变化,那就无法在遵守 Hooks 规则的情况下使用它。为了解决这个问题,React Query 创建了一个 useQueries 自定义钩子。

useQueries 可以让你动态调用任意数量的查询。语法如下:

jsx 复制代码
const queryResults = useQueries({
  queries: [
    { queryKey: ["api", "queryOne"], queryFn: fetchData },
    { queryKey: ["api", "queryTwo"], queryFn: fetchData }
  ]
})

queries 属性中接收一个查询数组。而且每个查询还可以设置自己的 options,在这里有一个心智模型:即这些查询也可以像单个 useQuery 查询一样进行自定义。useQueries 钩子将返回一个包含所有查询结果的数组。

了解了 useQueries 的工作原理,来实战一下:

jsx 复制代码
import { useQueries } from "@tanstack/react-query";
import "../App.css";
import { fetchData } from "../api";
import { Table } from "antd";

const params: Array<string> = ["iPhone", "OPPO", "MacBook"];

function DynamicParallelQueries() {
  const multipleQueries = useQueries({
    queries: params.map((q: string) => {
      return {
        queryKey: [{ queryIdentifier: "api", q }],
        queryFn: fetchData,
      };
    }),
  });

  const columns = [
    {
      title: "名称",
      dataIndex: "title",
      key: "title",
    },
    {
      title: "价格",
      dataIndex: "price",
      key: "price",
    },
    {
      title: "品牌",
      dataIndex: "brand",
      key: "brand",
    },
  ];

  return (
    <>
      {multipleQueries.map((item: any) =>
        item.isFetching ? (
          "Fetching data..."
        ) : (
          <Table
            dataSource={item?.data?.products ?? []}
            columns={columns}
            rowKey={"id"}
          />
        )
      )}
    </>
  );
}

export default DynamicParallelQueries;

查看页面:

是不是非常简单且优雅!

深入 QueryClient

前面已经说了,QueryClient 可以操作缓存数据,触发重新获取查询。QueryClient 可以用来做更多事情,例如以下问题:

  • 查询失效
  • 预取(Prefetching)
  • 查询取消

在组件中可以利用 useQueryClient 这个钩子来使用 QueryClient

查询过滤器

在开始学习查询失效之前,有一点要注意,其中一些方法,也就是我们将要看到的方法,可以接收某些查询过滤器,以帮助你匹配正确的查询。

来看这个示例:

jsx 复制代码
import { Button } from "antd";
import DynamicParallelQueries from "./components/DynamicParallelQueries";
import { useQueryClient } from "@tanstack/react-query";
// import ManualParallelQueries from "./components/ManualParallelQueries";

function App() {
  const queryClient = useQueryClient();

  const handleOnRefetch = () => {
    queryClient.refetchQueries({ queryKey: [{ queryIdentifier: "api" }] });
  };

  return (
    <>
      <Button onClick={handleOnRefetch} type="primary">
        RefetchQueries
      </Button>
      {/* <ManualParallelQueries /> */}
      <DynamicParallelQueries />
    </>
  );
}

export default App;

refetchQueries 方法提供过滤器,可以重新获取所有符合查询关键字["api"]或以查询关键字["api"]开头的查询。

现在,除 queryKey 外,还可以使用更多过滤器。在 QueryClient 方法中使用的过滤器通常称为 QueryFilters,支持通过以下方式进行过滤:

  • 查询 Keys
  • 查询 type
  • 查询是过时的还是新鲜的
  • fetchStatus
  • predicate函数

下面是一些使用 QueryFilters 的示例:

jsx 复制代码
queryClient.refetchQueries({ type: "active" })

在下面的示例中,重新获取所有现在被视为过期(staleTime 已过期)的查询:

jsx 复制代码
queryClient.refetchQueries({ stale: true })

在下面的示例中,我们使用以值为"idle"的 fetchStatus 过滤器来重新获取当前没有获取任何内容的所有查询:

jsx 复制代码
queryClient.refetchQueries({ fetchStatus: "idle"})

使用 predicate 属性并向其传递一个匿名函数,该函数将接收正在验证的查询,并访问其当前状态;如果该状态为 "error",则函数将返回 true,意思是当前状态为 error 的所有查询都将重新获取:

jsx 复制代码
queryClient.refetchQueries({ 
    predicate: (query) => query.state.status === "error" 
})

还可以组合过滤器,重新获取了查询关键字以["api"]开头的所有过时查询。:

jsx 复制代码
queryClient.refetchQueries({ queryKey: ["api"], stale: true })

如果不想传递任何过滤器,并希望该方法适用于所有查询,则可以选择不传递任何过滤器:

jsx 复制代码
queryClient.refetchQueries()

这将重新获取所有查询。

查询失效

有时,数据过期可能与配置的 staleTime 无关,因为可能是你执行了突变(Mutation)操作,也有可能是其他用户做了类似的操作。这时,可以利用 QueryClientinvalidateQueries 方法将查询标记为过期。

下面是 invalidateQueries 方法的语法:

jsx 复制代码
queryClient.invalidateQueries({ queryKey: ["api"] })

通过调用 invalidateQueries,所有与["api"]匹配或以["api"]开头的查询都会被标记为过期,如果配置了 staleTime ,则会覆盖其过期时间。如果你的查询因为渲染的 useQuery 钩子正在使用而处于活跃状态,那么 React Query 将负责重新获取该查询。

现在让我们通过下面的示例来实践一下:

jsx 复制代码
import { Button, Space } from "antd";
import DynamicParallelQueries from "./components/DynamicParallelQueries";
import { useQueryClient } from "@tanstack/react-query";
// import ManualParallelQueries from "./components/ManualParallelQueries";

function App() {
  const queryClient = useQueryClient();

  const handleOnRefetch = () => {
    queryClient.refetchQueries({ queryKey: [{ queryIdentifier: "api" }] });
  };

  const handleOnQueryInvalidation = () => {
    queryClient.invalidateQueries({
      queryKey: [{ queryIdentifier: "api" }],
    });
  };

  return (
    <>
      <Space>
        <Button onClick={handleOnRefetch} type="primary">
          重新查询
        </Button>
        <Button onClick={handleOnQueryInvalidation} type="primary">
          查询失效
        </Button>
      </Space>

      {/* <ManualParallelQueries /> */}
      <DynamicParallelQueries />
    </>
  );
}

export default App;

点击查询失效按钮将使所有匹配或包含 [{ queryIdentifier: "api" }] 作为其 queryKey 一部分的查询无效,查询数据将立即被标记为过时数据,将在后台自动重新请求。

数据预取

当你可以预测用户可能想做的事情必然会触发查询时,你就可以利用 prefetchQuery 预取查询,为用户节省一些时间。

以下是 prefetchQuery 方法的语法:

jsx 复制代码
queryClient.prefetchQuery({
   queryKey: ["api"],
   queryFn: fetchData
});

prefetchQuery 尝试获取数据,并将其缓存到给定的 queryKey 下。这是一个异步方法,因此需要使用 async/await

来一个实际例子:

jsx 复制代码
import { Button, Space } from "antd";
import { useQueryClient } from "@tanstack/react-query";
import { fetchData } from "./api";
import { useState } from "react";
import PrefetchedDataComponent from "./components/PrefetchedDataComponent";

function App() {
  const [renderComponent, setRenderComponent] = useState(false);
  const queryClient = useQueryClient();

  const handleOnPrefetchData = async () => {
    await queryClient.prefetchQuery({
      queryKey: [{ queryIdentifier: "api", q: "Samsung" }],
      queryFn: fetchData,
      staleTime: 60000,
    });
  };

  return (
    <>
      <Space>
        <Button
          onMouseEnter={handleOnPrefetchData}
          onClick={() => setRenderComponent(true)}
        >
          预取查询
        </Button>
      </Space>
      {renderComponent ? <PrefetchedDataComponent /> : null}
    </>
  );
}

export default App;

我们在 prefetchData 函数中调用 prefetchQuery 方法,并将返回的数据缓存在 [{ queryIdentifier: "api", q: "Samsung" }] queryKey 下。我们还将 staleTime 设为 1 分钟,因此在调用此查询后,数据将在 1 分钟内保持新鲜。

PrefetchedDataComponent 组件:

jsx 复制代码
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchData } from "../api";
import { Table } from "antd";

const PrefetchedDataComponent = () => {
  const queryClient = useQueryClient();

  const eData = queryClient.getQueryData([
    { queryIdentifier: "Samsung_api", q: "Samsung" },
  ]) as any;

  const _products = eData?.products ?? [];

  const { data } = useQuery({
    queryKey: [{ queryIdentifier: "Samsung_api", q: "Samsung" }],
    queryFn: fetchData,
    enabled: _products.length === 0,
  });

  const columns = [
    {
      title: "名称",
      dataIndex: "title",
      key: "title",
    },
    {
      title: "价格",
      dataIndex: "price",
      key: "price",
    },
    {
      title: "品牌",
      dataIndex: "brand",
      key: "brand",
    },
  ];

  return (
    <Table dataSource={data?.products ?? []} columns={columns} rowKey={"id"} />
  );
};

export default PrefetchedDataComponent;
  1. 我们预测用户点击按钮会希望获取数据,所以,我们让鼠标移入的时候触发数据预取。数据预取完成后,将缓存在 [{ queryIdentifier: "api", username: "userOne" }] queryKey 下。

  2. 当用户点击按钮时,PrefetchedDataComponent 组件渲染。由 [{ queryIdentifier: "api", username: "userOne" }] queryKey 标识的 useQuery 钩子已经缓存了数据并将其标记为新鲜数据,持续时间为一分钟,因此不需要触发数据请求,用户看到的是预取数据的渲染。

React Query 官方也提供了一个非常好的例子:传送门

取消查询

有时,你可能希望 useQuery 钩子在进行查询时可以取消。默认情况下,一旦你的 Promise resolve,该查询数据仍将被返回和缓存。但有时候,出于某种原因,你的组件在数据请求中途卸载,你可能希望取消查询。React Query 可以通过 自动或者手动 取消查询来解决这个问题。

为了实现取消查询,React Query 使用了 signal,它可以与 DOM 请求通信并中止它们,也就是 AbortSignal 对象,它源自于 AbortController Web APIAbortController - Web APIs | MDN)。

AbortSignalQueryFunctionContext 注入到我们的查询函数中,然后由我们的数据获取客户端使用。

以下是我们如何利用 axiosAbortSignal

jsx 复制代码
const fetchData = async ({ queryKey, signal }) => {
  const { username } = queryKey[0];
  
  const { data } = await axios.get(
    `https://danieljcafonso.builtwithdark.com/react-query-api/${username}`,
    { signal }
  );
  
  return data;
};

我们从 QueryFunctionContext 接收 signal,并在进行 get 请求时将其传入 axios client。

如果在使用 GraphQL 的场景中使用 fetch 或 graphql-request 等 axios 的替代方法,也需要向客户端传递 AbortSignal。这就是使用 fetch 的方法:

jsx 复制代码
const fetchDataWithFetch = async ({ queryKey, signal }) => {
  const { username } = queryKey[0];
  
  const response = await fetch(
    `https://danieljcafonso.builtwithdark.com/react-query-api/${username}`,
    { signal }
  );
  
  if (!response.ok) throw new Error("Something failed in your request");
  
  return response.json();
};

如果你使用的是 graphql-request 等 GraphQL client,也可以这样做:

jsx 复制代码
const fetchGQL = async ({signal}) => {
  const endpoint = <Add_Endpoint_here>;
  const client = new GraphQLClient(endpoint);
  
  const {
    posts: { data },
  } = await client.request({document: customQuery, signal});
  
  return data;
};

手动取消查询

要手动取消查询,QueryClient 提供了 cancelQueries 方法。下面是 cancelQueries 方法的语法:

jsx 复制代码
queryClient.cancelQueries({ queryKey: ["api"] })

调用 cancelQueries 后,当前正在获取并已收到 AbortSignal 的所有与["api"]匹配或以["api"]开头的查询都将被终止。

自动取消查询

当使用钩子的组件卸载,而你的查询正在获取数据时,如果你向 client 传递 AbortSignal,React Query 将通过取消承诺来中止你的查询。

让我们通过下面的示例看看 React Query 如何利用 AbortSignal 取消查询。首先,我们从配置查询函数开始:

jsx 复制代码
const fetchData = async ({ queryKey, signal }) => {
  const { username } = queryKey[0];
  
  const { data } = await axios.get(
    `https://danieljcafonso.builtwithdark.com/react-query-api/${username}`,
    { signal }
  );
  
  return data;
};

在前面的代码段中,我们创建了一个 fetchData 函数,该函数将接收 QueryContextObject。我们从中获取 signal,并将其传递给我们的 axios 客户端。

现在,让我们看看我们的组件:

jsx 复制代码
const ExampleQueryCancelation = () => {
  const [renderComponent, setRenderComponent] = useState(false);
  
  return (
    <div>
      <button onClick={() => setRenderComponent(!renderComponent)}>
        Render Component
      </button>
      {renderComponent ? <QueryCancelation /> : null}
    </div>
  );
};

在前面的代码段中,我们创建了一个 fetchData 函数,该函数将接收 QueryContextObject。我们从中获取信号,并将其传递给我们的 axios 客户端。

现在,让我们看看我们的组件:

jsx 复制代码
const QueryCancelation = () => {
  const { data } = useQuery({
    queryKey: [{ queryIdentifier: "api", username: "userOne" }],
    queryFn: fetchData,
  });
  
  const queryClient = useQueryClient();
  
  return (
    <div>
      <p>{data?.hello}</p>
      <button
        onClick={() =>
          queryClient.cancelQueries({
            queryKey: [{ queryIdentifier: "api" }],
          })
        }
      >
        Cancel Query
      </button>
    </div>
  );
};

该代码段向我们展示了 QueryCancelation 组件。在该组件中,我们将执行以下操作:

  1. 我们创建一个由 [{ queryIdentifier: "api", username: "userOne" }] 查询键标识的查询。
  2. 我们访问 QueryClient。
  3. 我们从查询中呈现数据。
  4. 我们将呈现一个按钮,点击该按钮后,QueryClient 将取消所有键值匹配或包含 [{ queryIdentifier: "api" }] 的查询。

现在让我们回顾一下这些组件的生命周期以及取消查询的工作原理:

  1. 我们渲染 ExampleQueryCancelation 组件。
  2. 我们点击按钮来呈现 QueryCancelation 组件。
  3. QueryCancelation 呈现后,其 useQuery 钩子将触发一个请求,以获取其数据。
  4. 在这个请求过程中,我们点击右后方的按钮来呈现 QueryCancelation。
  5. 由于我们的请求尚未解决,而且我们的组件已卸载,React Query 会终止我们的信号,从而取消我们的请求。
  6. 我们点击按钮,再次渲染 QueryCancelation 组件。
  7. QueryCancelation 已呈现,其 useQuery 钩子将触发请求以获取其数据。
  8. 在请求过程中,我们点击按钮取消查询。这将迫使 React Query 中止我们的信号,并再次取消请求。

至此,我们已经了解了 QueryClient 及其部分方法如何帮助我们解决一些常见的服务器状态难题。

分页查询

在处理大型数据集的接口时,为了避免让前端一次性处理所有数据,分页请求是一种常用的方法。只需使用 useQuery 及其选项之一 keepPreviousData 即可。

让我们看看接下来的示例,然后了解分页和 React Query 是如何工作的。首先,我们从查询函数开始:

jsx 复制代码
const fetchData = async ({ queryKey }) => {
  const { page } = queryKey[0];
  
  const { data } = await axios.get(
    `https://danieljcafonso.builtwithdark.com/react-query-paginated?page=${page}&results=10`
  );
  
  return data;
};

然后,构建一个用于显示和获取分页数据的组件:

jsx 复制代码
const PaginatedQuery = () => {
  const [page, setPage] = useState(0);
  const { isLoading, isError, error, data, isFetching, isPreviousData } =
    useQuery({
      queryKey: [{ queryIdentifier: "api", page }],
      queryFn: fetchData,
      keepPreviousData: true,
    });
    
  if (isLoading) {
    return <h2>Loading initial data...</h2>;
  }
  
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  
  return (
    <>
      <div>
        {data.results.map((user) => (
          <div key={user.email}>
            {user.name.first}
            {user.name.last}
          </div>
        ))}
      </div>
      <div>
      <button
        onClick={() => setPage((oldValue) => oldValue === 0
          ? 0 : oldValue - 1)}
        disabled={page === 0}
      >
        Previous Page
      </button>
      <button
        disabled={isPreviousData}
        onClick={() => {
          if (!isPreviousData) setPage((old) => old + 1);
        }}
      >
        Next Page
      </button>
      </div>
      {isFetching ? <span> Loading...</span> : null}
    </>
  );
};

让我们按顺序回顾一下前面代码块中发生的事情:

  1. 我们创建一个状态变量来保存当前选定的页面。
  2. 我们创建查询,其中包含 [{ queryIdentifier: "api", page }] 查询键、作为查询函数的 fetchData 函数,并将 keepPreviousData 设置为 true。我们将该选项设置为 true,是因为默认情况下,只要我们的查询键发生变化,查询数据也会随之变化;现在,由于我们有一个分页的 API,所以即使我们更换页面,我们也希望继续显示数据。
  3. 然后,我们对 isLoading、isError、error、data、isFetching 和 isPreviousData 进行解构。
  4. 然后,我们有两个 if 语句,分别用于显示查询正在加载或出现错误。
  5. 如果有数据,我们将显示数据和两个按钮,分别用于移动到下一页和上一页。移动到下一页的按钮利用 isPreviousData 来确保在我们点击它并移动到下一个查询后,该按钮将被禁用。我们还会显示一个获取指示器。

既然我们已经了解了代码的结构,那就来看看它在交互时的表现吧:

  1. 我们的组件被渲染,并开始获取第一个页面。

isLoading 属性被设置为 true,因此我们渲染了 Loading 初始数据。

  1. 第一页的数据已解析,因此我们要显示它。

  2. 我们点击 "下一页 "按钮:

    1. 页面值递增。
    2. 查询键发生变化,因此下面的查询开始获取数据。
    3. 由于我们将 keepPreviousData 设为 true,因此仍将显示旧数据。
    4. 由于我们显示的是旧数据,因此 isPreviousData 设置为 true,下一页按钮也被禁用。
    5. 获取指示器显示正在加载。
  3. 我们获取并显示新数据。

  4. 点击上一页按钮:

    1. 页面值会递减。
    2. 查询键回到上一页。
    3. 由于数据已缓存在该查询键下,因此会返回。
    4. 由于数据已经过时,因此会触发新的获取请求。
    5. 获取指示器显示 "加载中"。
  5. 接收并显示新数据。

正如你所看到的,你只需要一个 keepPreviousData 和 useQuery,就能实现分页功能。

无限查询

另一种非常常见的需求是无限滚动组件,我们会看到一个列表,它允许我们在向下滚动时加载更多数据。React Query 提供了 useQuery 的替代方法,即另一个名为 useInfiniteQuery 的自定义钩子。

使用 useInfiniteQuery 钩子与 useQuery 钩子有许多相似之处,但也有一些不同之处需要我们注意:

  1. 你的数据现在是一个包含以下内容的对象:

    1. 获取的页面
    2. 用于获取页面的页面参数
  2. 用于获取下一页的名为 fetchNextPage 的函数

  3. 用于获取上一页的名为 fetchPreviousPage 的函数

  4. 一个名为 isFetchingNextPage 的布尔值状态,用于指示是否正在获取下一个页面

  5. 一个名为 isFetchingPreviousPage 的布尔值状态,表示正在获取下一页内容

  6. 一个名为 hasNextPage 的布尔值状态,用于指示列表是否有下一页面

  7. 一个名为 hasPreviousPage 的布尔值状态,用于指示列表是否有上一页

最后两个布尔值取决于可以传递给钩子的两个选项。它们分别是 getNextPageParamgetPreviousPageParam。这两个函数将负责选取缓存中的最后一页或第一页,并检查其数据是否指示要获取的下一页或上一页。如果这些值存在,则相应的布尔值为 true。如果返回未定义值,则布尔值为false。

要使用 useInfiniteQuery 挂钩,需要这样导入:

jsx 复制代码
import { useInfiniteQuery } from "@tanstack/react-query"

现在让我们看一个如何使用 useInfiniteQuery 挂钩构建无限列表的示例:

jsx 复制代码
const fetchData = async ({ pageParam = 1 }) => {
    const { data } = await axios.get(
        `https://danieljcafonso.builtwithdark.com/react-query-infinite?page=${pageParam}&results=10`
    );
    
    return data;
  };

钩子将在 QueryFunctionContext 中传递 pageParam,以便我们利用它来获取数据。与 useQuery 钩子中的查询函数一样,该查询函数需要解析数据或抛出错误,因此之前学到的所有原则都适用。

下一个代码段将展示我们的 InfiniteScroll 组件:

jsx 复制代码
const InfiniteScroll = () => {
  const {
    isLoading,
    isError,
    error,
    data,
    fetchNextPage,
    isFetchingNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ["api"],
    queryFn: fetchData,
    getNextPageParam: (lastPage, pages) => {
      return lastPage.info.nextPage;
    },
  });
  
  if (isLoading) {
    return <h2>Loading initial data...</h2>;
  }
  
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  
  return (
    <>
      <div>
        {data.pages.map((page) =>
          page.results.map((user) => (
            <div key={user.email}>
              {user.name.first}
              {user.name.last}
            </div>
          ))
        )}
      </div>
      <button
        disabled={!hasNextPage || isFetchingNextPage}
        onClick={fetchNextPage}
      >
        {isFetchingNextPage
          ? <<Loading...>>
          : hasNextPage
          ? <<Load More>>
          : <<You have no more data>>}
      </button>
    </>
  );
};

在前面的代码段中,我们有一个组件,可以渲染一个无限列表。这就是我们在组件中要做的事情:

  1. 我们创建了 useInfiniteQuery,它以 ["api"] 作为查询关键字,以 fetchData 作为查询函数。它还在 getNextPageParam 选项中接收了一个匿名函数,用于检查下一页是否还有更多数据需要加载。

  2. 我们还从钩子中重组了一些构建应用程序所需的变量。

  3. 然后,我们使用两个 if 语句来显示查询正在加载或出现错误。

  4. 有了数据后,我们就将内容映射到页面属性中,以呈现我们的列表。

  5. 我们还将呈现一个按钮,如果没有下一页,或者正在获取下一页时,该按钮将被禁用。点击后,该按钮将获取更多数据。这个按钮的信息也将取决于一些约束条件: 2. 如果我们正在获取数据,下一页将显示加载信息 3. 如果我们有下一个页面,它将显示 "加载更多",以便用户点击它开始获取数据 4. 如果没有更多数据可提取,它将显示一条信息,让用户知道没有更多数据了

刚才我们回顾了组件的构建过程,现在让我们看看在与组件交互时它将如何工作:

  1. 我们的组件渲染后,会自动获取列表的第一页:isLoading 属性设置为 true,因此我们渲染正在加载的初始数据

  2. 列表第一页的数据已解析,因此我们将其显示出来。

  3. 同时,getNextPageParam 函数会检查列表中是否有更多数据。

  4. 如果没有更多数据,则 hasNextPage 属性将设为 false,获取更多数据的按钮将被禁用,并显示 "您没有更多数据"。

  5. 如果有更多数据,则 hasNextPage 属性设置为 true,用户可以点击按钮获取更多数据。

  6. 如果用户点击了按钮,我们将看到以下内容:

    1. 下一页开始获取。
    2. isFetchingNextPage 的值变为 true。
    3. 按钮被禁用,并显示加载信息。
    4. 数据已解析,我们的数据页属性长度增加,因为它拥有新页面的数据。重复步骤 3、4 和 5。

用 Devtools 调试查询

在之前,我们已经学习了 React Query Devtools。当时,你还不知道如何使用查询,所以我们无法看到它的工作原理,现在我们可以了。

我们将利用在 "动态并行查询" 部分展示 useQueries 钩子示例时编写的代码,代码如下:

jsx 复制代码
const usernameList = ["userOne", "userTwo", "userThree"];

const ExampleTwo = () => {
  const multipleQueries = useQueries({
    queries: usernameList.map((username) => {
      return {
        queryKey: [{ queryIdentifier: "api", username }],
        queryFn: fetchData,
      };
    }),
  });
  
  return (
    <div>
      {multipleQueries.map(({ data, isFetching }) => (
        <p>{isFetching ? "Fetching data..." : data.hello}
          </p>
      ))}
    </div>
  );
};

使用该代码并检查我们的页面时,Devtools 会向我们展示以下内容:

在上图中,我们可以看到以下内容:

  1. 我们有三个查询
  2. 每个查询都由各自的查询密钥标识
  3. 所有查询当前都已过时
  4. 我们选择了以 [{ queryIdentifier: "api", username: "userThree" }] 查询键标识的查询

选择查询后,我们可以在 "查询详情 "选项卡中看到查询详情。

在上图中,我们可以看到该查询由其查询密钥和状态标识。

在查询详细信息选项卡上向下滚动,我们还可以看到以下内容:

在上图中,我们可以看到可以对所选查询执行多种操作,如重新获取、失效、重置和删除。

我们还可以查看该查询的当前数据。

进一步向下滚动 "查询详细信息"选项卡,我们还可以查看 "查询资源管理器":

在上图中,我们可以看到所选查询的查询资源管理器。在这里,我们可以看到查询正在使用的选项。在这里,我们可以看到该查询的默认 cacheTime 为 300000。

现在你已经知道在 Devtools 中可以看到每个选定查询的内容了。

在结束本节之前,让我们看看点击查询详细信息操作中的某个按钮会发生什么:

在上图中,我们点击了由 [{ queryIdentifier: "api", username: "userTwo" }] 查询键标识的查询的无效按钮。

正如你在学习查询失效时所记得的,当我们失效一个查询时,它会被自动标记为过时查询,如果该查询当前正在渲染,则会被自动重新刷新。如图所示,情况就是这样。我们的查询已经失效,所以没有必要再次将其标记为失效,但由于它当前正在我们的页面上呈现,React Query 会负责重新抓取它,我们可以从图中看到这一点。

正如您在本节中所看到的,Devtools 可以为您节省大量调试查询的时间。通过查看查询内部,您可以检查其数据是否配置了正确的选项,甚至可以根据需要触发一些操作。

总结

在本章中,我们进一步了解了如何使用 useQuery 钩子来解决处理服务器状态时遇到的一些常见问题。现在,你可以轻松处理所有数据获取需求了。

你了解了并行查询,并学会了使用 useQuery 手动创建这些查询。我们还向你介绍了 useQuery 钩子的一种替代方法:useQueries。通过它,你学会了如何构建动态并行查询。

您将进一步了解 QueryClient 的一些方法,这些方法允许您预取、取消和失效查询,您还将了解如何利用 QueryFilters 来定制这些方法中使用的查询匹配。

分页是一种典型的 UI 模式,现在您知道了在 useQuery 及其选项的帮助下可以轻松创建分页组件。

另一种典型的用户界面模式是无限滚动。在另一个名为 useInfiniteQuery 的 useQuery 变体的帮助下,您了解了 React Query 如何让您构建一个具有无限列表的应用程序。

最后,您使用 React Query Devtools 查看了查询内部,了解了它如何让您调试查询并改进开发流程。

在第 6 章 "使用 React 查询执行数据突变 "中,我们将抛开数据获取,转而讨论数据突变。您将了解 React Query 如何在名为 useMutation 的自定义钩子的帮助下执行突变。您还将利用这个钩子来处理应用程序中更常见的服务器状态挑战,并开始在应用程序中使用乐观更新来构建更好的用户体验。

相关推荐
Rowrey2 小时前
react+typescript,初始化与项目配置
javascript·react.js·typescript
谢尔登2 小时前
Vue 和 React 的异同点
前端·vue.js·react.js
风清云淡_A8 小时前
【react18】如何使用useReducer和useContext来实现一个todoList功能
react.js
化作繁星10 小时前
React 高阶组件的优缺点
前端·javascript·react.js
贩卖纯净水.15 小时前
REACT学习DAY02(恨连接不上服务器)
服务器·学习·react.js
一路向前的月光17 小时前
react(9)-redux
前端·javascript·react.js
谢尔登18 小时前
Vue 和 React 响应式的区别
前端·vue.js·react.js
程序员远仔21 小时前
【Vue.js 和 React.js 的主要区别是什么?】
前端·javascript·css·vue.js·react.js·性能优化·html5
小刘不知道叫啥1 天前
React源码揭秘 | 启动入口
前端·react.js·前端框架
程序员小续2 天前
Excel 表格和 Node.js 实现数据转换工具
前端·javascript·react.js·前端框架·vue·excel·reactjs