在第一篇我们讲到了ReactQuery的各种状态,有了这个基础我们就可以更好的学习useQuery的各种用法啦。本文是ReactQuery系列的第二篇"结合场景理解useQuery的各种用法",旨在结合场景让你快速理解useQuery的用法
useQuery 可以说 ReactQuery 的核心 API 了,useQuery 主要是用来查询并且默认会将查询到的数据缓存 起来,并且相同 queryKey 的查询可以共享缓存中的数据
发起一个简单查询
和查询相关的配置有很多,其中最重要的莫过于 queryKey 和 queryFn 了:
queryKey 是 useQuery 中最重要的配置之一,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())
有了 queryKey、queryFn 这两个配置就可以发起查询了,当组件挂载时且缓存中没有对应的新鲜的缓存,默认会发起请求
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-control 的 max-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: 使用了
placeholderData,isLoading不再会变成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 页面 query 的 initialData 页面立刻就展示出来了
但是有个问题是 useQuery 默认会将查询的时间作为缓存的更新时间 ,并不是 list 缓存的更新时间,这样我们很有可能会使用到旧的数据
如何处理这个问题呢?答案就是使用 initialDataUpdatedAt, initialDataUpdatedAt 同样支持传递一个函数 ,通过✅获取 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
给查询添加依赖
默认查询会在组件挂载时发起查询,但有的时候查询依赖某个参数,这个参数是异步的或者是等待⌛️用户输入,这时怎么办呢?
可以使用 useQuery 的 enabled 配置,它是一个 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 可以传递给 fetch 的 option,像这样:
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,
});

实现查询预加载
有时候我们希望能够在用户访问之前提前加载好数据,这样等用户真正访问的时候可以一瞬间就能加载完成✅
下面举一个具体的例子,假如你有一个列表,你想在用户鼠标 hover 到 item 上去的时候就立刻发出 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 的返回值中你可以拿到 isLoading、isError、data、isSuccess 这些状态,你可以使用 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