在第一篇我们讲到了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