迁移到React Query 3
以前版本的 React Query 很棒,并为库带来了一些惊人的新特性、更多的魔力以及更好的整体体验。 社区还为 React Query 带来了许多建议,同样也带来了对 React Query 的大量改进(issues/contributions)。 v3 版本 merge 了许多性能优化和 API 调优的内容。
概览
- 更具可扩展性和可测试性的缓存配置
- 更好的 SSR 支持
- 可以在任何地方的数据延迟(Data-lag,以前的 usePaginatedQuery)
- 双向(Bi-directional)无限查询
- 查询数据选择器!
- 在使用前为查询和/或修改配置完整的默认值
- 更加细粒度的可选渲染优化
- 新用途
useQueriesHook!(可变长度的并行查询) - 查询过滤器对
useIsFetching()Hook 的支持! - 修改对于重试/离线/重放的支持
- 修改/查询可以在 React 之外实现观察者模式
- 随时随地使用 React Query 核心逻辑!
- 通过使用
react-query/devtools,随时享受 Devtools - 持久化缓存到 web 的存储 中 (实验性,
react-query/persistQueryClient-experimental和react-query/createWebStoragePersistor-experimental)
破坏性修改
QueryCache 已被分解进 QueryClient 和较低级别的 QueryCache 和 MutationCache 实例。
QueryCache 包含所有查询,MutationCache 包含所有修改,而 QueryClient 可用于设置配置并与其进行交互。
这带来了一些好处:
- 允许使用不同类型的缓存
- 具有不同配置的多个 clinet 可以使用相同的缓存
- Clinets 可以跟踪查询,这些查询可用在 SSR 上共享缓存
- 客户端 API 更侧重于一般用法
- 更容易进行独立于各个组件的测试
当 new QueryClient() 时,如果不提供 QueryCache 和 MutationCache 对象,则会自动为你创建
1import { QueryClient } from "react-query";2 3const queryClient = new QueryClient();ReactQueryConfigProvider 和 ReactQueryCacheProvider 已被 QueryClientProvider 替代
现在可以在 QueryClient 中指定查询和修改的默认参数:
注意,现在这是一个默认选项(defaultOptions),而不是一个默认配置(defaultConfig)
1const queryClient = new QueryClient({2 defaultOptions: {3 queries: {4 // query options5 },6 mutations: {7 // mutation options8 },9 },10});QueryClientProvider 组件现在用于将 QueryClient 连接到应用:
1import { QueryClient, QueryClientProvider } from "react-query";2 3const queryClient = new QueryClient();4 5function App() {6 return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;7}默认的 QueryCache 已被移除,彻底
如先前所提到的,不再有从主包创建或导出的默认 QueryCache。
你必须通过 new QueryClient() 或 new QueryCache() 自己创建一个(后一个可以稍后通过 new QueryClient({ queryCache }) 传入)
不推荐使用的 makeQueryCache 已被删除
已经有很长时间了,但是终于消失了 :)
QueryCache.prefetchQuery() 被 QueryClient.prefetchQuery() 替代
新的 QueryClient.prefetchQuery() 函数是异步的,但不会从查询中返回数据。如果你需要数据,使用新的 QueryClient.fetchQuery() 函数
1// Prefetch a query:2await queryClient.prefetchQuery("posts", fetchPosts);3 4// Fetch a query:5try {6 const data = await queryClient.fetchQuery("posts", fetchPosts);7} catch (error) {8 // 错误处理9}ReactQueryErrorResetBoundary 和 QueryCache.resetErrorBoundaries() 被 QueryErrorResetBoundary 和 useQueryErrorResetBoundary() 替代
它们一起提供了与以前相同的体验,但增加了对手动选择要重置的组件树的控制。 有关更多信息,请参见:
QueryCache.getQuery() 被 QueryCache.find() 替代
QueryCache.find() 现在用于从一个缓存中查找单个查询
QueryCache.getQueries() 合并进 QueryCache.findAll()
QueryCache.findAll() 现在用于从一个缓存中查找多个查询
QueryCache.isFetching 合并进 QueryClient.isFetching()
请注意,它现在是作为函数而不是属性存在
useQueryCache hook 被 useQueryClient hook 替代
它返回为组件树中提供的 queryClient,除了重命名外无需进行太多调整。
查询的键值部分不再作为参数自动传递到查询函数
现在建议使用闭包将参数传递给查询函数:
1// Old2useQuery(["post", id], (_key, id) => fetchPost(id));3 4// New5useQuery(["post", id], () => fetchPost(id));如果仍然坚持不使用闭包,则可以使用新的 QueryFunctionContext:
1useQuery(["post", id], (context) => fetchPost(context.queryKey[1]));无限查询的分页参数现在使用 QueryFunctionContext.pageParam 传递
它们以前是作为查询函数中的最后一个参数添加的,但事实证明,这对于实现某些模式很是困难
1// Old2useInfiniteQuery(["posts"], (_key, pageParam = 0) => fetchPosts(pageParam));3 4// New5useInfiniteQuery(["posts"], ({ pageParam = 0 }) => fetchPosts(pageParam));不推荐使用 usePaginatedQuery(),而是应该选择 keepPreviousData 参数
新的 keepPreviousData 选项可用于 useQuery 和 useInfiniteQuery,它们对于数据具有相同的“滞后”效果:
1import { useQuery } from "react-query";2 3function Page({ page }) {4 const { data } = useQuery(["page", page], fetchPage, {5 keepPreviousData: true,6 });7}useInfiniteQuery() 现在是双向的
useInfiniteQuery() 的相关接口已更改为完全支持双向无限列表。
options.getFetchMore重命名为options.getNextPageParamqueryResult.canFetchMore重命名为queryResult.hasNextPagequeryResult.fetchMore重命名为queryResult.fetchNextPagequeryResult.isFetchingMore重命名为queryResult.isFetchingNextPage- 添加新的
options.getPreviousPageParam选项 - 添加新的
queryResult.hasPreviousPage属性 - 添加新的
queryResult.fetchPreviousPage属性 - 添加新的
queryResult.isFetchingPreviousPage - 无尽查询的
data现在是一个包裹了pages和pageParams的对象,用于获取分页数据:{ pages: [data, data, data], pageParams: [...]}
一个方向:
1const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =2 useInfiniteQuery(3 "projects",4 ({ pageParam = 0 }) => fetchProjects(pageParam),5 {6 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,7 },8 );双向:
1const {2 data,3 fetchNextPage,4 fetchPreviousPage,5 hasNextPage,6 hasPreviousPage,7 isFetchingNextPage,8 isFetchingPreviousPage,9} = useInfiniteQuery(10 "projects",11 ({ pageParam = 0 }) => fetchProjects(pageParam),12 {13 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,14 getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,15 },16);反向:
1const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =2 useInfiniteQuery(3 "projects",4 ({ pageParam = 0 }) => fetchProjects(pageParam),5 {6 select: (data) => ({7 pages: [...data.pages].reverse(),8 pageParams: [...data.pageParams].reverse(),9 }),10 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,11 },12 );现在,无限查询数据包含页面数组和用于获取这些页面的 pageParams
这样可以更轻松地操作数据和页面参数,例如,删除数据的第一页及其相关的参数:
1queryClient.setQueryData("projects", (data) => ({2 pages: data.pages.slice(1),3 pageParams: data.pageParams.slice(1),4}));useMutation 现在返回一个对象而不是一个数组
尽管旧的接口给我们带来了类似于初次发现 useState 时产生的那种温暖但模糊的感觉,但是这种感觉并没有持续多久。
现在,它返回是单个对象。
1// Old:2const [mutate, { status, reset }] = useMutation();3 4// New:5const { mutate, status, reset } = useMutation();mutation.mutate 不再返回一个 Promise
[mutate]变量已替换为mutation.mutate函数- 添加新的
mutation.mutateAsync函数
我们收到了许多问题反馈,因为用户希望这个 Promise 的行为表现得像一个正常的 Promise
现在这个 mutate 函数被分解为 mutate 和 mutateAsync 函数
因而,现在 mutate 函数的接口为回调函数式:
1const { mutate } = useMutation(addTodo);2 3mutate("todo", {4 onSuccess: (data) => {5 console.log(data);6 },7 onError: (error) => {8 console.error(error);9 },10 onSettled: () => {11 console.log("settled");12 },13});异步时,考虑使用 mutateAsync 函数:
1const { mutateAsync } = useMutation(addTodo);2 3try {4 const data = await mutateAsync("todo");5 console.log(data);6} catch (error) {7 console.error(error);8} finally {9 console.log("settled");10}useQuery 配置方式改变
1// Old:2useQuery({3 queryKey: "posts",4 queryFn: fetchPosts,5 config: { staleTime: Infinity },6});7 8// New:9useQuery({10 queryKey: "posts",11 queryFn: fetchPosts,12 staleTime: Infinity,13});如果设置,则 QueryOptions.enabled 选项必须为布尔值 (true/false)
现在,enabled 的查询选项将仅在值为 false 时禁用。
如果需要,可以使用 !!userId 或 Boolean(userId) 强制转换值,如果传递了非布尔值,则会抛出一个明显的语法的错误。
QueryOptions.initialStale 选项已被移除
initialStale 选项已被移除,初始的数据现在被视为常规数据。
这意味着,如果提供 initialData,则默认查询将在挂载时重新获取。
如果你不想该动作被立即执行,则可以定义一个 staleTime。
QueryOptions.forceFetchOnMount 选项被 refetchOnMount: 'always' 替代
说实话,我们增加了太多的 refetchOn____ 选项,所以这应该能解决问题。
QueryOptions.refetchOnMount 选项现在仅适用于其父组件,而不是所有查询观察者
当 refetchOnMount 设置为 false 时,任何其他组件都不能在挂载时重新获取数据。
在 v3 中,只有设置了该选项的组件不会在挂载时重新获取。
QueryOptions.queryFnParamsFilter 已被删除,以支持新的 QueryFunctionContext 对象
queryFnParamsFilter 选项已删除,因为查询函数现在通过获取 QueryFunctionContext 对象而不是通过查询键值。
由于 QueryFunctionContext 还包含查询键值,因此仍可以在查询函数本身中过滤参数。
QueryOptions.notifyOnStatusChange 选项已被 notifyOnChangeProps 和 notifyOnChangePropsExclusions 选项替代
有了这些新选项,可以在更细腻的程度上来配置组件重新渲染的逻辑。
只有当 data 或 error 被改变时重新渲染:
1import { useQuery } from "react-query";2 3function User() {4 const { data } = useQuery("user", fetchUser, {5 notifyOnChangeProps: ["data", "error"],6 });7 return <div>Username: {data.username}</div>;8}在 isStale 改变时忽略,不进行重新渲染:
1import { useQuery } from "react-query";2 3function User() {4 const { data } = useQuery("user", fetchUser, {5 notifyOnChangePropsExclusions: ["isStale"],6 });7 return <div>Username: {data.username}</div>;8}QueryResult.clear() 方法重命名为 QueryResult.remove()
尽管它被称为"clear",但实际上只是从高速缓存中删除了单个查询。 现在,该名称与其功能匹配。
QueryResult.updatedAt 拆分为 QueryResult.dataUpdatedAt 和 QueryResult.errorUpdatedAt 属性
由于可以同时显示数据和错误,因此 updatedAt 属性已拆分为 dataUpdatedAt 和 errorUpdatedAt。
setConsole() 被新函数 setLogger() 替代
1import { setLogger } from "react-query";2 3// 使用 Sentry4setLogger({5 error: (error) => {6 Sentry.captureException(error);7 },8});9 10// 使用 Winston11setLogger(winston.createLogger());React Native 不再需要重写 Logger
为了防止查询失败时在 React Native 中显示错误的界面,必须手动更改控制台:
1import { setConsole } from "react-query";2 3setConsole({4 log: console.log,5 warn: console.warn,6 error: console.warn,7});在 v3 中,React Query 在 React Native 中使用时,会自动做到上面这点
Typescript 的类型
QueryStatus 的类型已经从 enum 切换为 union type
因此,如果你要根据QueryStatus枚举属性检查查询或修改的状态,你现在必须检查其字符串字面量,而不是以前的枚举值。
因此,你必须将枚举属性更改为其等效的字符串文字,如下所示:
QueryStatus.Idle->'idle'QueryStatus.Loading->'loading'QueryStatus.Error->'error'QueryStatus.Success->'success'
1- import { useQuery, QueryStatus } from 'react-query';2+ import { useQuery } from 'react-query';3 4const { data, status } = useQuery(['post', id], () => fetchPost(id));5 6- if (status === QueryStatus.Loading) {7+ if (status === 'loading') {8 ...9}10 11- if (status === QueryStatus.Error) {12+ if (status === 'error') {13 ...14}新特性
查询数据选择器
现在,useQuery 和 useInfiniteQuery Hook 分别带有一个 select 选项,用于选择或转换部分查询结果。
1import { useQuery } from "react-query";2 3function User() {4 const { data } = useQuery("user", fetchUser, {5 select: (user) => user.username,6 });7 return <div>Username: {data}</div>;8}将 notifyOnChangeProps 选项设置为 ['data','error'] ,使得仅在所选数据更改时重新渲染。
useQueries() hook,用于可变长度的并行查询
想在循环内运行 useQuery 吗?
Hook 规则说不,但是有了新的 useQueries() hook,可以了!
1import { useQueries } from "react-query";2 3function Overview() {4 const results = useQueries([5 { queryKey: ["post", 1], queryFn: fetchPost },6 { queryKey: ["post", 2], queryFn: fetchPost },7 ]);8 return (9 <ul>10 {results.map(({ data }) => data && <li key={data.id}>{data.title})</li>)}11 </ul>12 );13}修改对重试/离线的支持
默认情况下,React Query 不会自动重试错误的修改,但是可以使用 retry 选项:
1const mutation = useMutation(addTodo, {2 retry: 3,3});如果由于设备离线而导致修改失败,那么当设备重新连接时,它们将以相同的顺序重新尝试。
持久化修改
现在可以将修改持久化到数据库或其他什么存储方式中,并在以后恢复。更多信息可以在修改中找到。
Observer
QueryObserver
QueryObserver 可以用来创建和/或监视一个查询:
1const observer = new QueryObserver(queryClient, { queryKey: "posts" });2 3const unsubscribe = observer.subscribe((result) => {4 console.log(result);5 unsubscribe();6});InfiniteQueryObserver
InfiniteQueryObserver 可以用来创建和/或监视一个无限查询:
1const observer = new InfiniteQueryObserver(queryClient, {2 queryKey: "posts",3 queryFn: fetchPosts,4 getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,5 getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,6});7 8const unsubscribe = observer.subscribe((result) => {9 console.log(result);10 unsubscribe();11});QueriesObserver
QueriesObserver 可以用来创建和/或监视多个查询:
1const observer = new QueriesObserver(queryClient, [2 { queryKey: ["post", 1], queryFn: fetchPost },3 { queryKey: ["post", 2], queryFn: fetchPost },4]);5 6const unsubscribe = observer.subscribe((result) => {7 console.log(result);8 unsubscribe();9});为特定查询/修改设置默认选项
查询
QueryClient.setQueryDefaults() 方法可用于为特定查询设置默认选项:
1queryClient.setQueryDefaults("posts", { queryFn: fetchPosts });2 3function Component() {4 const { data } = useQuery("posts");5}修改
QueryClient.setMutationDefaults() 方法可用于为特定修改设置默认选项:
1queryClient.setMutationDefaults("addPost", { mutationFn: addPost });2 3function Component() {4 const { mutate } = useMutation("addPost");5}useIsFetching()
现在,useIsFetching() Hook 可以接收一些过滤器,这些过滤器可用于例如仅显示某些查询类型的微调:
1const fetches = useIsFetching(["posts"]);分离
现在,React Query 的核心与 React 完全分离,这意味着它也可以独立使用或在其他框架中使用。
使用 react-query/core 入口点仅导入核心功能:
1import { QueryClient } from "react-query/core";Devtools 现在是主仓库和 npm 软件包的一部分
devtools 现在包含在 react-query 下的 react-query/devtools 中。
只需将 react-query-devtools 导入替换为 react-query/devtools 即可