跳到主要内容

React-Query基本使用

react-query 基本使用介绍

useQuery

使用查询

  • queryKey 查询键 查询键必须是可序列化的 只要键改变,React-Query 就会自动触发重新获取。

Use Query Key Factories

const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}

每个级别都建立在另一个级别之上

// 🕺 remove everything related to the todos feature
queryClient.removeQueries({ queryKey: todoKeys.all });

// 🚀 invalidate all the lists
queryClient.invalidateQueries({ queryKey: todoKeys.lists() });

// 🙌 prefetch a single todo
queryClient.prefetchQueries({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodo(id),
});
  • queryFn 查询函数 查询函数实际上可以是任何返回 Promise 的函数。并应解析数据抛出错误

  • enabled 禁用\暂停查询

  • retry 查询重试次数

  • keepPreviousData 分页\滞后查询

const { isLoading, isError, error, data, isFetching, isPreviousData } =
useQuery({
queryKey: ["projects", page],
queryFn: () => fetchProjects(page),
keepPreviousData: true,
});
  • initialData 初始查询数据\预填充数据 数据会持久保存在缓存中
const result = useQuery({
queryKey: ["todo", todoId],
queryFn: () => fetch("/todos"),
initialData: () => {
// 您可以从另一个查询的缓存结果中为查询提供初始数据
return queryClient.getQueryData(["todos"])?.find((d) => d.id === todoId);
},
// 更新初始数据
initialDataUpdatedAt: () =>
queryClient.getQueryState(["todos"])?.dataUpdatedAt,
});
  • placeholderData 占位符查询数据 数据不会持久保存到缓存中

useMutation

  • mutationFn 突变函数

  • onMutate(variables) 突变触发

  • onError(error, variables, context) 突变错误

  • onSuccess(data, variables, context) 突变成功

  • onSettled(data, error, variables, context) 突变结束

// ✅ use an object for multiple variables
const mutation = useMutation({
mutationFn: ({ title, body }) => updateTodo(title, body),
});
mutation.mutate({ title: "hello", body: "world" });

userQueries 动态并行查询

使用它来动态地并行执行任意数量的查询

function App({ users }) {
const userQueries = useQueries({
queries: users.map((user) => {
return {
queryKey: ["user", user.id],
queryFn: () => fetchUserById(user.id),
};
}),
});
}

useQueryClient

  • invalidateQueries 查询失效 指定要使哪些查询失效
queryClient.invalidateQueries({
queryKey: ["posts", id, "comments"],
exact: false,
});
  • setQueryData 更新查询缓存 将指定的数据更新至查询缓存中
useMutation({
mutationFn: (newTitle) =>
axios
.patch(`/posts/${id}`, { title: newTitle })
.then((response) => response.data),
// 💡 response of the mutation is passed to onSuccess
onSuccess: (data) => {
// ✅ update detail view directly
queryClient.setQueryData(["posts", id], data);
},
});

等待失效完成

{
// 🎉 是否等待查询失效完成
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ["posts", id, "comments"],
});
};
}
{
// 🚀 发后即忘 - 不会等待
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["posts", id, "comments"] });
};
}
  • prefetchQuery 预取查询结果
const prefetchTodos = async () => {
// 此查询的结果将像普通查询一样被缓存
await queryClient.prefetchQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
};
  • cancelQueries 手动取消查询
queryClient.cancelQueries({ queryKey: ["todos"] });

useIsFetching 获取是否存在接口正在查询

使用示例

接口封装

import { useQuery, useMutation, QueryClient } from "@tanstack/react-query";

export const getZenCache = (params, options) => {
return useQuery(
["zen", params],
() => axios.get(`https://api.github.com/zen?id=${str}`),
{
select: (res) => res?.data || {}, // 防止对象取值不存在时的报错
...options,
}
);
};

组件中使用

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

return useMutation({
mutationFn: (params) => new Promise((resolve) => resolve(params)),
onSuccess: (data, variables, context) => {
// 使查询失效,重新调用接口
queryClient.invalidateQueries({ queryKey: ["zen"] });
// queryClient.setQueryData(["zen"], { data: "dasdasd" });
},
});
};

const ApiCom = () => {
const query = getZenCache(id);
const editMutate = MutateCom();

const handleMutate = () => {
editMutate.mutate("666", {
onSuccess: (res) => {
console.log("success", res);
},
});
};

return (
<>
{query.isFetching && "loading..."}
{query.isError && query.error?.message}
{query?.data?.data}
<button onClick={handleMutate}>触发修改</button>
</>
);
};

项目最佳实践

📦useQuery封装示例

import { useQuery } from 'react-query';

import { axios } from '@/lib/axios';
import { ExtractFnReturnType, QueryConfig } from '@/lib/react-query';

import { Comment } from '../types';

export const getComments = ({ discussionId }: { discussionId: string }): Promise<Comment[]> => {
return axios.get(`/comments`, {
params: {
discussionId,
},
});
};

type QueryFnType = typeof getComments;

type UseCommentsOptions = {
discussionId: string;
config?: QueryConfig<QueryFnType>;
};

export const useComments = ({ discussionId, config }: UseCommentsOptions) => {
return useQuery<ExtractFnReturnType<QueryFnType>>({
queryKey: ['comments', discussionId],
queryFn: () => getComments({ discussionId }),
...config,
});
};

📦useMutate封装示例

// 删除示例, 配合乐观更新
import { useMutation } from 'react-query';

import { axios } from '@/lib/axios';
import { MutationConfig, queryClient } from '@/lib/react-query';
import { useNotificationStore } from '@/stores/notifications';

import { Comment } from '../types';

export const deleteComment = ({ commentId }: { commentId: string }) => {
return axios.delete(`/comments/${commentId}`);
};

type UseDeleteCommentOptions = {
discussionId: string;
config?: MutationConfig<typeof deleteComment>;
};

export const useDeleteComment = ({ config, discussionId }: UseDeleteCommentOptions) => {
const { addNotification } = useNotificationStore();
return useMutation({
// 若你不需要乐观更新, 那么大多数时候, 你不会使用onMutate
onMutate: async (deletedComment) => {
// 取消接口查询请求, 这在执行乐观更新时最有用
// 取消任何返回查询结果重新提取,以便它们在解析时不会破坏您的乐观更新。
await queryClient.cancelQueries(['comments', discussionId]);

// 获取上一次查询的值, 用于做乐观更新
const previousComments = queryClient.getQueryData<Comment[]>(['comments', discussionId]);

// 乐观更新, 直接对上一次查询的数据做解析
queryClient.setQueryData(
['comments', discussionId],
previousComments?.filter((comment) => comment.id !== deletedComment.commentId)
);

return { previousComments };
},
// 配合乐观更新对数据做处理
onError: (_, __, context: any) => {
// 报错处理, 取消乐观更新的数据, 返回上一次的数据解析
if (context?.previousComments) {
queryClient.setQueryData(['comments', discussionId], context.previousComments);
}
},
onSuccess: () => {
// 成功结果处理, 使查询缓存失效, 重新调用接口查询
queryClient.invalidateQueries(['comments', discussionId]);
addNotification({
type: 'success',
title: 'Comment Deleted',
});
},
...config,
mutationFn: deleteComment,
});
};

// 更新示例, 配合数据重新获取: refetchQueries
import { useMutation } from '@tanstack/react-query';

import { axios } from '@/lib/axios';
import { MutationConfig, queryClient } from '@/lib/react-query';
import { useNotificationStore } from '@/stores/notifications';

import { Discussion } from '../types';

export type UpdateDiscussionDTO = {
data: {
title: string;
body: string;
};
discussionId: string;
};

export const updateDiscussion = ({
data,
discussionId,
}: UpdateDiscussionDTO): Promise<Discussion> => {
return axios.patch(`/discussions/${discussionId}`, data);
};

type UseUpdateDiscussionOptions = {
config?: MutationConfig<typeof updateDiscussion>;
};

export const useUpdateDiscussion = ({ config }: UseUpdateDiscussionOptions = {}) => {
const { addNotification } = useNotificationStore();

return useMutation({
onSuccess: (data) => {
// 用于根据特定条件重新获取数据
queryClient.refetchQueries(['discussion', data.id]);
addNotification({
type: 'success',
title: 'Discussion Updated',
});
},
...config,
mutationFn: updateDiscussion,
});
};

页面示例

import { ArchiveIcon } from '@heroicons/react/outline';

import { Spinner, MDPreview } from '@/components/Elements';
import { User } from '@/features/users';
import { useAuth } from '@/lib/auth';
import { POLICIES, Authorization } from '@/lib/authorization';
import { formatDate } from '@/utils/format';

import { useComments } from '../api/getComments';

import { DeleteComment } from './DeleteComment';

type CommentsListProps = {
discussionId: string;
};

export const CommentsList = ({ discussionId }: CommentsListProps) => {
const { user } = useAuth();
const commentsQuery = useComments({ discussionId });

// 只有在初始化第一次进入页面的时候展示加载状态
if (commentsQuery.isLoading) {
return (
<div className="w-full h-48 flex justify-center items-center">
<Spinner size="lg" />
</div>
);
}

// 对空状态展示的处理
if (!commentsQuery?.data?.length)
return (
<div
role="list"
aria-label="comments"
className="bg-white text-gray-500 h-40 flex justify-center items-center flex-col"
>
<ArchiveIcon className="h-10 w-10" />
<h4>No Comments Found</h4>
</div>
);

// 这里做了乐观更新
return (
<ul aria-label="comments" className="flex flex-col space-y-3">
{commentsQuery.data.map((comment, index) => (
<li
aria-label={`comment-${comment.body}-${index}`}
key={comment.id || index}
className="w-full bg-white shadow-sm p-4"
>
<Authorization policyCheck={POLICIES['comment:delete'](user as User, comment)}>
<div className="flex justify-between">
<span className="text-xs font-semibold">{formatDate(comment.createdAt)}</span>
<DeleteComment discussionId={discussionId} id={comment.id} />
</div>
</Authorization>

<MDPreview value={comment.body} />
</li>
))}
</ul>
);
};