【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

相关推荐
老码沉思录6 小时前
React Native 全栈开发实战班 - 数据管理与状态之Zustand应用
javascript·react native·react.js
老码沉思录7 小时前
React Native 全栈开发实战班 :数据管理与状态之React Hooks 基础
javascript·react native·react.js
我认不到你7 小时前
antd proFromSelect 懒加载+模糊查询
前端·javascript·react.js·typescript
凹凸曼打不赢小怪兽10 小时前
react 受控组件和非受控组件
前端·javascript·react.js
鑫宝Code11 小时前
【React】状态管理之Redux
前端·react.js·前端框架
2401_8576100315 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
fighting ~16 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录16 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
老码沉思录17 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js
老码沉思录20 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js