迁移到 React Query 3
React Query 的早期版本非常出色,为该库带来了诸多惊艳的新特性、更多魔法效果以及整体更佳的使用体验。它们也推动了库的大规模采用,并随之带来了海量的打磨反馈(问题报告与贡献),从而揭示出一些亟待进一步优化的环节,以让库变得更为完善。v3 版本正是这些打磨成果的集中体现。
概览
- 更具可扩展性且更易测试的缓存配置
- 更完善的 SSR 支持
- 数据滞后(此前通过 usePaginatedQuery 实现)现已支持任意位置!
- 双向无限查询(bi-directional Infinite Queries)
- 查询数据选择器(Query data selectors)!
- 可在使用前为查询和/或修改全面配置默认选项
- 更精细的可选渲染优化粒度
- 全新
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 可用于设置配置并与其交互。
此举带来如下优势:
- 支持不同类型缓存。
- 多个具有不同配置的客户端可共享同一缓存。
- 客户端可用于跟踪查询,从而在 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 // Error handling9}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。
这使得数据与 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({ mutationFn: 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})使用 async/await 时可调用 mutateAsync 函数:
1const { mutateAsync } = useMutation({ mutationFn: 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// Log with Sentry4setLogger({5 error: (error) => {6 Sentry.captureException(error)7 },8})9 10// Log with Winston11setLogger(winston.createLogger())React Native 不再需要手动覆写日志器
为避免 React Native 中查询失败时显示错误界面,此前需手动修改 Console:
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 已从 枚举 更改为 联合类型
因此,若你此前通过 QueryStatus 枚举属性检查查询或修改的 status 属性,则现需改为检查该枚举此前各属性所对应的字符串字面量。
故需将枚举属性替换为其等效字符串字面量,例如:
QueryStatus.Idle→'idle'QueryStatus.Loading→'loading'QueryStatus.Error→'error'QueryStatus.Success→'success'
以下为你需进行的变更示例:
1- import { useQuery, QueryStatus } from 'react-query'; // [!code --]2+ import { useQuery } from 'react-query'; // [!code ++]3 4const { data, status } = useQuery(['post', id], () => fetchPost(id))5 6- if (status === QueryStatus.Loading) { // [!code --]7+ if (status === 'loading') { // [!code ++]8 ...9}10 11- if (status === QueryStatus.Error) { // [!code --]12+ if (status === 'error') { // [!code ++]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({2 mutationFn: addTodo,3 retry: 3,4})若修改因设备离线而失败,设备重连后将按原顺序重试。
持久化修改
修改现可持久化至存储并在后续恢复。更多信息请参阅修改文档。
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({ mutationKey: ['addPost'] })5}useIsFetching()
useIsFetching() Hook 现支持过滤器,例如可仅对特定类型查询显示加载指示器:
1const fetches = useIsFetching({ queryKey: ['posts'] })核心分离
React Query 的核心现已完全与 React 解耦,因此亦可独立使用或集成至其他框架。仅导入核心功能,请使用 react-query/core 入口点:
1import { QueryClient } from 'react-query/core'开发者工具现已整合至主仓库及 npm 包
开发者工具现已直接包含于 react-query 包中,导入路径为 react-query/devtools。只需将 react-query-devtools 导入替换为 react-query/devtools 即可。