【ReactQuery】结合场景理解useQuery的各种用法

在第一篇我们讲到了ReactQuery的各种状态,有了这个基础我们就可以更好的学习useQuery的各种用法啦。本文是ReactQuery系列的第二篇"结合场景理解useQuery的各种用法",旨在结合场景让你快速理解useQuery的用法

useQuery 可以说 ReactQuery 的核心 API 了,useQuery 主要是用来查询并且默认会将查询到的数据缓存 起来,并且相同 queryKey 的查询可以共享缓存中的数据

发起一个简单查询

和查询相关的配置有很多,其中最重要的莫过于 queryKeyqueryFn 了:

queryKeyuseQuery 中最重要的配置之一,ReactQuery 会根据这个数组生成一个哈希值作为查询的标识,相同哈希值的查询将会被视为相同的查询可以共享缓存中的数据

什么样的 queryKey 会被视为相同的?

数组中的成员顺序相同,如果数组中有某个成员是对象,则这个对象自己的成员的顺序不同不会影响queryKey的生成唯一ID的结果,如下面👇这三个 key 都会被视为相同的(关于这一点官方文档也有说明)

jsx 复制代码
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })

queryFn 是一个返回 Promise 的函数,像这样:

jsx 复制代码
() => fetch("/api/user", {
	method: "POST",
	body: JSON.stringify({
		id: query.id,
	}),
}).then((res) => res.json())

有了 queryKeyqueryFn 这两个配置就可以发起查询了,当组件挂载时且缓存中没有对应的新鲜的缓存,默认会发起请求

tsx 复制代码
function User() {
  const { query } = useRouter();
  const userQuery = useQuery({
    queryKey: ["user", query.id],
    queryFn: () =>
      fetch("/api/user", {
        method: "POST",
        body: JSON.stringify({
          id: query.id,
        }),
      }).then((res) => res.json()),
    staleTime: 1000 * 5, // 5秒后数据将过期
  });

  return (
    <div className="p-5">
      <h1 className=" text-2xl mb-5">user info</h1>
      <div>
        {userQuery.isLoading ? (
          <p>Loading....</p>
        ) : (
          <div className="p-4 border border-gray-200 w-64 flex flex-col justify-center">
            <div className="text-wrap w-64">
              name: {userQuery.data.data.name}
            </div>
            <div className="text-wrap w-64">age: {userQuery.data.data.age}</div>
          </div>
        )}
      </div>
    </div>
  );
}

给查询添加过期时间

ReactQuery 的缓存可以说是核心功能之一了,但是默认缓存会立刻 变成过期状态 stale,这不是白白浪费了这强大的功能?

Tip: 关于ReactQuery默认在什么时机会发起查询,可以看我第一篇文章【ReactQuery】理解ReactQuery的中的状态

如果我们想让 Query 的缓存更久一点怎么办那就要祭出我们的 stateTime 配置了,它决定了缓存何时变得不新鲜有点类似 HTTP 缓存的 cache-controlmax-age

tsx 复制代码
const userQuery = useQuery({
	queryKey: ["user", query.id],
	queryFn: () =>
		fetch("/api/user", {
			method: "POST",
			body: JSON.stringify({
				id: query.id,
			}),
		}).then((res) => res.json()),
	staleTime: 1000 * 5, // 5秒后数据将过期
});

提到了 staleTime 不得不提一下 useQuery 的另一个配置 cacheTime,它们两个分别代表什么呢?

  • stateTime:就决定了缓存中的数据多久会变得不新鲜(不新鲜的数据但仍存在于缓存
  • cacheTime: 决定了缓存的存储时间,cacheTime 过期后的数据会从缓存中删除

给查询添加初始值

有的时候我们希望给数据添加默认值,在请求回来之前使用初始数据来渲染 UI,实现一个占位效果避免视觉抖动

useQuery 中怎么实现初始值的配置呢?在 useQuery 中可以使用 placeholderData 这个查询配置项

来看看配置了 placeholderData 的效果,placeholderData 在初始时展示同时会立刻获取数据 拿到数据后替换 placeholderData

Tip: 使用了 placeholderDataisLoading 不再会变成 true,因为 isLoading 只有在初次加载且没有数据的情况下为 true

用已有缓存做初始值

有的时候我们希望从已有的其他查询的缓存 中获取数据📊,作为查询的初始值以达到更快加载页面的目的

举个例子,假如我有一个列表的数据由若干个Item组成,点击进去 Item 可以查看 Item 的详情,并且 Item 详情的数据和列表对应 Item 一致 ,此时我们就可以通过在 initialData 函数中获取 list[id] 中的缓存数据作为初始值

tsx 复制代码
const list: ListItem[] = [
  {
    id: 1,
    title: "今天大事件11111111111",
    content:
      "事件的具体内容..............................................................",
  },
  {
    id: 2,
    title: "今天大事件22222222222",
    content:
      "事件的具体内容..............................................................",
  },
];

如何实现将其他查询的缓存的值作为当前 query 的 initialData 呢?那就需要使用 initialData 了,initialData 支持传递一个函数

tsx 复制代码
function Detail() {
  const { query } = useRouter();
  const queryClient = useQueryClient();
  const detailQuery = useQuery({
    queryKey: ["user", Number(query.id)],
    queryFn: () => fetchDetail(Number(query.id)),
    initialData: () => {
      const list = queryClient.getQueryData(["list"]);
      if (list) {
        const detail = (list as any[]).find(
          (item) => item.id == Number(query.id)
        );
        if (detail) return detail;
      }
    },
    staleTime: 1000 * 60,
  });
  return (
    <div className="p-5">
      {detailQuery.isLoading ? (
        <p>Loading...</p>
      ) : (
        <>
          <h1 className=" text-2xl mb-5">{detailQuery.data.title}</h1>
          <p className="  text-base">{detailQuery.data.content}</p>
        </>
      )}
    </div>
  );
}

可以看到由于我从缓存里拿出了 list 中对应的某个 item 的数据作为 detail 页面 queryinitialData 页面立刻就展示出来了

但是有个问题是 useQuery 默认会将查询的时间作为缓存的更新时间 ,并不是 list 缓存的更新时间,这样我们很有可能会使用到旧的数据

如何处理这个问题呢?答案就是使用 initialDataUpdatedAtinitialDataUpdatedAt 同样支持传递一个函数 ,通过✅获取 list 缓存的更新时间作为 initialDataUpdatedAt 即可解决这个问题

tsx 复制代码
const detailQuery = useQuery({
    queryKey: ["user", Number(query.id)],
    queryFn: () => fetchDetail(Number(query.id)),
    initialData: () => {
      const list = queryClient.getQueryData(["list"]);
      if (list) {
        const detail = (list as any[]).find(
          (item) => item.id == Number(query.id)
        );
        if (detail) return detail;
      }
    },
    staleTime: 1000 * 10,
    initialDataUpdatedAt() {
      // 获取list 查询的状态
      const queryState = queryClient.getQueryState(["list"]);
      if (queryState) {
        console.log(queryState.dataUpdatedAt, "queryState.dataUpdatedAt");
        return queryState.dataUpdatedAt;
      }
    },
  });

Tip: placeholderData 也可以作为初始值,为什么不用呢?因为 placeholderData 数据不会被缓 存,在这个场景下我希望重复利用缓存因此使用 initialData

给查询添加依赖

默认查询会在组件挂载时发起查询,但有的时候查询依赖某个参数,这个参数是异步的或者是等待⌛️用户输入,这时怎么办呢?

可以使用 useQueryenabled 配置,它是一个 boolean 值(默认 true),当为 true 时可以发起查询 false 时则不会发起查询

来看个具体例子🌰,假如我有一个搜索🔍页面,我希望当用户输入值之后才开始查询

这时我们可以把用户输入的 keyworld 转为 boolean 值作为 enabled 的值,当 keyworld 有值了才发起查询

tsx 复制代码
function Search() {
  const queryClient = useQueryClient();
  const [keyworld, setKeyworld] = useState("");
  const searchQuery = useQuery({
    queryKey: ["search", keyworld],
    queryFn: () => searchContent(keyworld),
    enabled: !!keyworld,
  });
  
  const debounceSetKeyworld = useDebounce(setKeyworld);

  useEffect(() => {
    if (!keyworld) {
      queryClient.setQueryData(["search", keyworld], undefined);
    }
  }, [keyworld]);

  return (
    <div className="flex justify-center items-start w-screen h-screen">
      <div className="flex flex-col justify-center items-center mt-40">
        <h1 className="text-2xl mb-2">搜索内容</h1>
        <div className=" flex flex-row items-center gap-x-2 mb-4">
          <input
            onChange={(e) => {
              debounceSetKeyworld(e.target.value.trim());
            }}
            className="border border-gray-300 rounded indent-2"
            placeholder="请输入内容"
          />
        </div>

        <div className=" self-start mt-2">
          {searchQuery.isLoading && searchQuery.isPending ? (
            <p>Loading...</p>
          ) : null}

          {searchQuery.isSuccess ? (
            <div className="space-y-2">
              {searchQuery.data.map((content: any) => {
                return (
                  <div
                    className="space-y-2 border border-r-gray-200 px-2"
                    key={content.id}
                  >
                    <p className=" text-base">{content.content}</p>
                  </div>
                );
              })}
            </div>
          ) : null}
        </div>
      </div>
    </div>
  );
}

export default Search;

实现取消查询

还是拿上面搜索的例子假设搜索是一个很耗时的操作,用户输入错了一个关键词然后删除,那之前的查询其实是没有任何作用,在 useQuery 有取消的办法吗?

ReactQuery 会给每一个 queryFn 注入一个参数 signal(利用了 AbortSignal),如果你使用是 fetch 可以传递给 fetchoption,像这样:

tsx 复制代码
  const searchQuery = useQuery({
    queryKey: ["search", keyworld],
    queryFn: ({ signal }) => {
      return fetch(`/api/search?keyworld=${keyworld}`, { signal })
        .then((res) => res.json())
        .then((res) => res.data);
    },
    enabled: !!keyworld,
  });

实现查询预加载

有时候我们希望能够在用户访问之前提前加载好数据,这样等用户真正访问的时候可以一瞬间就能加载完成✅

下面举一个具体的例子,假如你有一个列表,你想在用户鼠标 hoveritem 上去的时候就立刻发出 item 详情🔎页的查询,当用户访问详情是立刻就可以看到结果,在 ReactQuery 中如何实现?

要实现这个效果可以使用 useQueryClient 获取 ReactQuery 客户端实例,通过✅queryClient.prefetchQuery 来提前触发详情页的 query 的查询,这样当访问时就不用等待了因为已经被缓存

tsx 复制代码
import { useQuery, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import React from "react";

function Articles() {
  const queryClient = useQueryClient();
  const articlesQuery = useQuery({
    queryKey: ["article"],
    queryFn: () =>
      fetch("/api/articles", {
        method: "POST",
      })
        .then((res) => res.json())
        .then((res) => res.data),
    staleTime: 1000 * 60,
  });

  // 发起预请求
  const prefetchArticle = (id: number) => {
    queryClient.prefetchQuery({
      queryKey: ["article", String(id)],
      queryFn: () =>
        fetch("/api/article", {
          method: "POST",
          body: JSON.stringify({
            id,
          }),
        })
          .then((res) => res.json())
          .then((res) => res.data),
      staleTime: 1000 * 60,
    });
  };
  return (
    <div>
      {articlesQuery.isLoading ? (
        <p>Loading....</p>
      ) : (
        <>
          {articlesQuery.data.map((article: any) => {
            return (
              <Link
                href={{
                  pathname: "/article/[id]",
                  query: {
                    id: article.id,
                  },
                }}
                key={article.id}
              >
                <div
                  onMouseEnter={() => {
                    prefetchArticle(article.id);
                  }}
                  className="px-5 py-3 border rounded border-gray-200 cursor-pointer"
                >
                  <div className=" text-xl">title: {article.title}</div>
                  <div className=" text-base text-gray-500 line-clamp-2 text-ellipsis">
                    title: {article.title}
                  </div>
                </div>
              </Link>
            );
          })}
        </>
      )}
    </div>
  );
}

export default Articles;

给请求添加 loading 效果

useQuery 的返回值中你可以拿到 isLoadingisErrordataisSuccess 这些状态,你可以使用 isLoading 来判断是否显示指示器

tsx 复制代码
 const {isError, isLoading, data, isSuccess} = useQuery({
    queryKey: ["user", query.id],
    queryFn: () =>
      fetch("/api/user", {
        method: "POST",
        body: JSON.stringify({
          id: query.id,
        }),
      }).then((res) => res.json()),
    staleTime: 1000 * 5, // 5秒后数据将过期
  });

为什么使用 isLoading 来确定是否展示指示器?

这是因为 isLoading 只有在数据初次加载时才会变成 true,随后数据的重试、数据过期重新加载等操作 isLoading 不会再变,而isFetching在每次获取数据都会变化通常,如果使用isFetching,ReactQuery更新过期数据时突然页面变成loading状态会让用户看起来很疑惑🤔
文章的实例代码已经放到 GitHub - PaddyChen75/next-react-query-demo: react-query demo

相关推荐
知识分享小能手14 分钟前
React学习教程,从入门到精通,React AJAX 语法知识点与案例详解(18)
前端·javascript·vue.js·学习·react.js·ajax·vue3
NeverSettle_5 小时前
React工程实践面试题深度分析2025
javascript·react.js
学前端搞口饭吃5 小时前
react reducx的使用
前端·react.js·前端框架
努力往上爬de蜗牛5 小时前
react3面试题
javascript·react.js·面试
开心不就得了5 小时前
React 进阶
前端·javascript·react.js
谢尔登5 小时前
【React】React 哲学
前端·react.js·前端框架
学前端搞口饭吃8 小时前
react context如何使用
前端·javascript·react.js
GDAL8 小时前
为什么Cesium不使用vue或者react,而是 保留 Knockout
前端·vue.js·react.js
Dragon Wu18 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
YU大宗师18 小时前
React面试题
前端·javascript·react.js