框架
版本

迁移到 React Query 3

之前版本的 React Query 非常棒,并为库带来了令人惊叹的新特性、更多魔力以及整体更好的体验。它们也带来了大规模的采用,以及大量的改进(问题/贡献),并揭示了一些需要更多润色的地方,以使库变得更好。v3 包含了这些润色。

概述

  • 更具可扩展性和可测试性的缓存配置
  • 更好的 SSR 支持
  • 数据延迟(以前的 usePaginatedQuery)随处可用!
  • 双向无限查询
  • 查询数据选择器!
  • 在使用前完全配置查询和/或突变的默认值
  • 更精细的可选渲染优化
  • 新的 useQueries 钩子!(可变长度的并行查询执行)
  • useIsFetching() 钩子的查询过滤器支持!
  • 突变的重试/离线/重放支持
  • 在 React 之外观察查询/突变
  • 在任何你想要的地方使用 React Query 核心逻辑!
  • 通过 react-query/devtools 捆绑/共置 Devtools
  • 缓存持久化到 Web 存储(通过 react-query/persistQueryClient-experimentalreact-query/createWebStoragePersistor-experimental 进行实验)

重大变更

QueryCache 已被拆分为 QueryClient 以及更底层的 QueryCacheMutationCache 实例。

QueryCache 包含所有查询,MutationCache 包含所有突变,而 QueryClient 可用于设置配置并与它们交互。

这有一些好处

  • 允许不同类型的缓存。
  • 具有不同配置的多个客户端可以使用相同的缓存。
  • 客户端可以用于跟踪查询,这可以用于 SSR 上的共享缓存。
  • 客户端 API 更侧重于通用用法。
  • 更容易测试各个组件。

当创建一个 new QueryClient() 时,如果你不提供 QueryCacheMutationCache,它们会自动为你创建。

tsx
import { QueryClient } from 'react-query'

const queryClient = new QueryClient()
import { QueryClient } from 'react-query'

const queryClient = new QueryClient()

ReactQueryConfigProviderReactQueryCacheProvider 都已被 QueryClientProvider 替换

查询和突变的默认选项现在可以在 QueryClient 中指定

请注意,现在是 defaultOptions 而不是 defaultConfig

tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // query options
    },
    mutations: {
      // mutation options
    },
  },
})
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // query options
    },
    mutations: {
      // mutation options
    },
  },
})

QueryClientProvider 组件现在用于将 QueryClient 连接到你的应用程序

tsx
import { QueryClient, QueryClientProvider } from 'react-query'

const queryClient = new QueryClient()

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
import { QueryClient, QueryClientProvider } from 'react-query'

const queryClient = new QueryClient()

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

默认的 QueryCache 已经消失了。这次是真的!

正如之前在弃用说明中提到的,不再有从主包创建或导出的默认 QueryCache你必须通过 new QueryClient()new QueryCache() 创建你自己的 (然后你可以将其传递给 new QueryClient({ queryCache }) )

已弃用的 makeQueryCache 实用程序已被移除。

期待已久,但它终于消失了 :)

QueryCache.prefetchQuery() 已被移动到 QueryClient.prefetchQuery()

新的 QueryClient.prefetchQuery() 函数是异步的,但不返回查询中的数据。如果你需要数据,请使用新的 QueryClient.fetchQuery() 函数

tsx
// Prefetch a query:
await queryClient.prefetchQuery('posts', fetchPosts)

// Fetch a query:
try {
  const data = await queryClient.fetchQuery('posts', fetchPosts)
} catch (error) {
  // Error handling
}
// Prefetch a query:
await queryClient.prefetchQuery('posts', fetchPosts)

// Fetch a query:
try {
  const data = await queryClient.fetchQuery('posts', fetchPosts)
} catch (error) {
  // Error handling
}

ReactQueryErrorResetBoundaryQueryCache.resetErrorBoundaries() 已被 QueryErrorResetBoundaryuseQueryErrorResetBoundary() 替换。

总而言之,这些提供了与以前相同的体验,但增加了控制权,可以选择要重置的组件树。有关更多信息,请参阅

QueryCache.getQuery() 已被 QueryCache.find() 替换。

QueryCache.find() 现在应该用于从缓存中查找单个查询

QueryCache.getQueries() 已被移动到 QueryCache.findAll()

QueryCache.findAll() 现在应该用于从缓存中查找多个查询

QueryCache.isFetching 已被移动到 QueryClient.isFetching()

请注意,现在它是一个函数而不是属性

useQueryCache 钩子已被 useQueryClient 钩子替换。

它为其组件树返回提供的 queryClient,除了重命名之外,不需要太多调整。

查询键的组成部分/片段不再自动展开到查询函数中。

内联函数现在是向查询函数传递参数的建议方式

tsx
// Old
useQuery(['post', id], (_key, id) => fetchPost(id))

// New
useQuery(['post', id], () => fetchPost(id))
// Old
useQuery(['post', id], (_key, id) => fetchPost(id))

// New
useQuery(['post', id], () => fetchPost(id))

如果你仍然坚持不使用内联函数,你可以使用新传递的 QueryFunctionContext

tsx
useQuery(['post', id], (context) => fetchPost(context.queryKey[1]))
useQuery(['post', id], (context) => fetchPost(context.queryKey[1]))

无限查询页面参数现在通过 QueryFunctionContext.pageParam 传递

它们以前作为查询函数中的最后一个查询键参数添加,但这对于某些模式来说被证明是困难的

tsx
// Old
useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))

// New
useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam))
// Old
useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))

// New
useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam))

usePaginatedQuery() 已被移除,取而代之的是 keepPreviousData 选项

新的 keepPreviousData 选项可用于 useQueryuseInfiniteQuery,并且会对你的数据产生相同的“延迟”效果

tsx
import { useQuery } from 'react-query'

function Page({ page }) {
  const { data } = useQuery(['page', page], fetchPage, {
    keepPreviousData: true,
  })
}
import { useQuery } from 'react-query'

function Page({ page }) {
  const { data } = useQuery(['page', page], fetchPage, {
    keepPreviousData: true,
  })
}

useInfiniteQuery() 现在是双向的

useInfiniteQuery() 接口已更改为完全支持双向无限列表。

  • options.getFetchMore 已重命名为 options.getNextPageParam
  • queryResult.canFetchMore 已重命名为 queryResult.hasNextPage
  • queryResult.fetchMore 已重命名为 queryResult.fetchNextPage
  • queryResult.isFetchingMore 已重命名为 queryResult.isFetchingNextPage
  • 添加了 options.getPreviousPageParam 选项
  • 添加了 queryResult.hasPreviousPage 属性
  • 添加了 queryResult.fetchPreviousPage 属性
  • 添加了 queryResult.isFetchingPreviousPage
  • 无限查询的 data 现在是一个对象,其中包含 pages 和用于获取页面的 pageParams{ pages: [data, data, data], pageParams: [...]}

单向

tsx
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  useInfiniteQuery(
    'projects',
    ({ pageParam = 0 }) => fetchProjects(pageParam),
    {
      getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    },
  )
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  useInfiniteQuery(
    'projects',
    ({ pageParam = 0 }) => fetchProjects(pageParam),
    {
      getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    },
  )

双向

tsx
const {
  data,
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
} = useInfiniteQuery(
  'projects',
  ({ pageParam = 0 }) => fetchProjects(pageParam),
  {
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
  },
)
const {
  data,
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
} = useInfiniteQuery(
  'projects',
  ({ pageParam = 0 }) => fetchProjects(pageParam),
  {
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
  },
)

单向反向

tsx
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  useInfiniteQuery(
    'projects',
    ({ pageParam = 0 }) => fetchProjects(pageParam),
    {
      select: (data) => ({
        pages: [...data.pages].reverse(),
        pageParams: [...data.pageParams].reverse(),
      }),
      getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    },
  )
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  useInfiniteQuery(
    'projects',
    ({ pageParam = 0 }) => fetchProjects(pageParam),
    {
      select: (data) => ({
        pages: [...data.pages].reverse(),
        pageParams: [...data.pageParams].reverse(),
      }),
      getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    },
  )

无限查询数据现在包含页面数组和用于获取这些页面的 pageParams。

这允许更容易地操作数据和页面参数,例如,删除第一页数据及其参数

tsx
queryClient.setQueryData(['projects'], (data) => ({
  pages: data.pages.slice(1),
  pageParams: data.pageParams.slice(1),
}))
queryClient.setQueryData(['projects'], (data) => ({
  pages: data.pages.slice(1),
  pageParams: data.pageParams.slice(1),
}))

useMutation 现在返回一个对象而不是数组

虽然旧的方式让我们回想起第一次发现 useState 时的温暖感觉,但它们并没有持续多久。现在突变返回是一个单独的对象。

tsx
// Old:
const [mutate, { status, reset }] = useMutation()

// New:
const { mutate, status, reset } = useMutation()
// Old:
const [mutate, { status, reset }] = useMutation()

// New:
const { mutate, status, reset } = useMutation()

mutation.mutate 不再返回 promise

  • [mutate] 变量已更改为 mutation.mutate 函数
  • 添加了 mutation.mutateAsync 函数

我们收到了很多关于此行为的问题,因为用户期望 promise 的行为类似于常规 promise。

因此,mutate 函数现在被拆分为 mutatemutateAsync 函数。

当使用回调时,可以使用 mutate 函数

tsx
const { mutate } = useMutation({ mutationFn: addTodo })

mutate('todo', {
  onSuccess: (data) => {
    console.log(data)
  },
  onError: (error) => {
    console.error(error)
  },
  onSettled: () => {
    console.log('settled')
  },
})
const { mutate } = useMutation({ mutationFn: addTodo })

mutate('todo', {
  onSuccess: (data) => {
    console.log(data)
  },
  onError: (error) => {
    console.error(error)
  },
  onSettled: () => {
    console.log('settled')
  },
})

当使用 async/await 时,可以使用 mutateAsync 函数

tsx
const { mutateAsync } = useMutation({ mutationFn: addTodo })

try {
  const data = await mutateAsync('todo')
  console.log(data)
} catch (error) {
  console.error(error)
} finally {
  console.log('settled')
}
const { mutateAsync } = useMutation({ mutationFn: addTodo })

try {
  const data = await mutateAsync('todo')
  console.log(data)
} catch (error) {
  console.error(error)
} finally {
  console.log('settled')
}

useQuery 的对象语法现在使用折叠配置

tsx
// Old:
useQuery({
  queryKey: 'posts',
  queryFn: fetchPosts,
  config: { staleTime: Infinity },
})

// New:
useQuery({
  queryKey: 'posts',
  queryFn: fetchPosts,
  staleTime: Infinity,
})
// Old:
useQuery({
  queryKey: 'posts',
  queryFn: fetchPosts,
  config: { staleTime: Infinity },
})

// New:
useQuery({
  queryKey: 'posts',
  queryFn: fetchPosts,
  staleTime: Infinity,
})

如果设置,QueryOptions.enabled 选项必须是布尔值 (true/false)

enabled 查询选项现在仅在值为 false 时禁用查询。 如果需要,可以使用 !!userIdBoolean(userId) 转换值,如果传递非布尔值,则会抛出一个方便的错误。

QueryOptions.initialStale 选项已被移除

initialStale 查询选项已被删除,初始数据现在被视为常规数据。这意味着如果提供了 initialData,则查询默认会在挂载时重新获取。如果你不想立即重新获取,你可以定义一个 staleTime

QueryOptions.forceFetchOnMount 选项已被 refetchOnMount: 'always' 替换

老实说,我们积累了太多的 refetchOn____ 选项,所以这应该可以清理一下。

QueryOptions.refetchOnMount 选项现在仅应用于其父组件,而不是所有查询观察者

refetchOnMount 设置为 false 时,任何其他组件都会被阻止在挂载时重新获取。在版本 3 中,只有设置了该选项的组件才不会在挂载时重新获取。

QueryOptions.queryFnParamsFilter 已被移除,取而代之的是新的 QueryFunctionContext 对象。

queryFnParamsFilter 选项已被删除,因为查询函数现在获取 QueryFunctionContext 对象而不是查询键。

参数仍然可以在查询函数本身内进行过滤,因为 QueryFunctionContext 也包含查询键。

QueryOptions.notifyOnStatusChange 选项已被新的 notifyOnChangePropsnotifyOnChangePropsExclusions 选项取代。

使用这些新选项,可以配置组件应在精细级别重新渲染的时间。

仅在 dataerror 属性更改时重新渲染

tsx
import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery(['user'], fetchUser, {
    notifyOnChangeProps: ['data', 'error'],
  })
  return <div>Username: {data.username}</div>
}
import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery(['user'], fetchUser, {
    notifyOnChangeProps: ['data', 'error'],
  })
  return <div>Username: {data.username}</div>
}

isStale 属性更改时,阻止重新渲染

tsx
import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery(['user'], fetchUser, {
    notifyOnChangePropsExclusions: ['isStale'],
  })
  return <div>Username: {data.username}</div>
}
import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery(['user'], fetchUser, {
    notifyOnChangePropsExclusions: ['isStale'],
  })
  return <div>Username: {data.username}</div>
}

QueryResult.clear() 函数已重命名为 QueryResult.remove()

虽然它被称为 clear,但它实际上只是从缓存中删除了查询。名称现在与功能匹配。

QueryResult.updatedAt 属性已被拆分为 QueryResult.dataUpdatedAtQueryResult.errorUpdatedAt 属性

由于数据和错误可以同时存在,因此 updatedAt 属性已被拆分为 dataUpdatedAterrorUpdatedAt

setConsole() 已被新的 setLogger() 函数替换

tsx
import { setLogger } from 'react-query'

// Log with Sentry
setLogger({
  error: (error) => {
    Sentry.captureException(error)
  },
})

// Log with Winston
setLogger(winston.createLogger())
import { setLogger } from 'react-query'

// Log with Sentry
setLogger({
  error: (error) => {
    Sentry.captureException(error)
  },
})

// Log with Winston
setLogger(winston.createLogger())

React Native 不再需要覆盖 logger

为了防止在 React Native 中查询失败时显示错误屏幕,有必要手动更改 Console

tsx
import { setConsole } from 'react-query'

setConsole({
  log: console.log,
  warn: console.warn,
  error: console.warn,
})
import { setConsole } from 'react-query'

setConsole({
  log: console.log,
  warn: console.warn,
  error: console.warn,
})

在版本 3 中,当在 React Native 中使用 React Query 时,这是自动完成的

Typescript

QueryStatus 已从 枚举 更改为 联合类型

因此,如果你之前针对 QueryStatus 枚举属性检查查询或突变的状态属性,你现在必须针对枚举先前为每个属性保留的字符串字面量进行检查。

因此,你必须将枚举属性更改为等效的字符串字面量,如下所示

  • QueryStatus.Idle -> 'idle'
  • QueryStatus.Loading -> 'loading'
  • QueryStatus.Error -> 'error'
  • QueryStatus.Success -> 'success'

这是一个你必须做的更改的示例

tsx
- import { useQuery, QueryStatus } from 'react-query'; // [!code --]
+ import { useQuery } from 'react-query'; // [!code ++]

const { data, status } = useQuery(['post', id], () => fetchPost(id))

- if (status === QueryStatus.Loading) { // [!code --]
+ if (status === 'loading') { // [!code ++]
  ...
}

- if (status === QueryStatus.Error) { // [!code --]
+ if (status === 'error') { // [!code ++]
  ...
}
- import { useQuery, QueryStatus } from 'react-query'; // [!code --]
+ import { useQuery } from 'react-query'; // [!code ++]

const { data, status } = useQuery(['post', id], () => fetchPost(id))

- if (status === QueryStatus.Loading) { // [!code --]
+ if (status === 'loading') { // [!code ++]
  ...
}

- if (status === QueryStatus.Error) { // [!code --]
+ if (status === 'error') { // [!code ++]
  ...
}

新特性

查询数据选择器

useQueryuseInfiniteQuery 钩子现在有一个 select 选项,用于选择或转换查询结果的部分内容。

tsx
import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery(['user'], fetchUser, {
    select: (user) => user.username,
  })
  return <div>Username: {data}</div>
}
import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery(['user'], fetchUser, {
    select: (user) => user.username,
  })
  return <div>Username: {data}</div>
}

notifyOnChangeProps 选项设置为 ['data', 'error'],以便仅在选定的数据更改时重新渲染。

useQueries() 钩子,用于可变长度的并行查询执行

你是否希望可以在循环中运行 useQuery?Hook 的规则不允许这样做,但是有了新的 useQueries() Hook,你就可以做到了!

tsx
import { useQueries } from 'react-query'

function Overview() {
  const results = useQueries([
    { queryKey: ['post', 1], queryFn: fetchPost },
    { queryKey: ['post', 2], queryFn: fetchPost },
  ])
  return (
    <ul>
      {results.map(({ data }) => data && <li key={data.id}>{data.title})</li>)}
    </ul>
  )
}
import { useQueries } from 'react-query'

function Overview() {
  const results = useQueries([
    { queryKey: ['post', 1], queryFn: fetchPost },
    { queryKey: ['post', 2], queryFn: fetchPost },
  ])
  return (
    <ul>
      {results.map(({ data }) => data && <li key={data.id}>{data.title})</li>)}
    </ul>
  )
}

重试/离线突变

默认情况下,React Query 不会在错误时重试 mutation,但是可以使用 retry 选项来实现重试。

tsx
const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})
const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})

如果 mutation 因为设备离线而失败,当设备重新连接时,它们将按照相同的顺序重试。

持久化突变

现在可以将 mutation 持久化到存储并在稍后恢复。更多信息可以在 mutation 文档中找到。

QueryObserver

可以使用 QueryObserver 来创建和/或监视一个 query

tsx
const observer = new QueryObserver(queryClient, { queryKey: 'posts' })

const unsubscribe = observer.subscribe((result) => {
  console.log(result)
  unsubscribe()
})
const observer = new QueryObserver(queryClient, { queryKey: 'posts' })

const unsubscribe = observer.subscribe((result) => {
  console.log(result)
  unsubscribe()
})

InfiniteQueryObserver

可以使用 InfiniteQueryObserver 来创建和/或监视一个无限 query

tsx
const observer = new InfiniteQueryObserver(queryClient, {
  queryKey: 'posts',
  queryFn: fetchPosts,
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})

const unsubscribe = observer.subscribe((result) => {
  console.log(result)
  unsubscribe()
})
const observer = new InfiniteQueryObserver(queryClient, {
  queryKey: 'posts',
  queryFn: fetchPosts,
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})

const unsubscribe = observer.subscribe((result) => {
  console.log(result)
  unsubscribe()
})

QueriesObserver

可以使用 QueriesObserver 来创建和/或监视多个 query

tsx
const observer = new QueriesObserver(queryClient, [
  { queryKey: ['post', 1], queryFn: fetchPost },
  { queryKey: ['post', 2], queryFn: fetchPost },
])

const unsubscribe = observer.subscribe((result) => {
  console.log(result)
  unsubscribe()
})
const observer = new QueriesObserver(queryClient, [
  { queryKey: ['post', 1], queryFn: fetchPost },
  { queryKey: ['post', 2], queryFn: fetchPost },
])

const unsubscribe = observer.subscribe((result) => {
  console.log(result)
  unsubscribe()
})

为特定查询设置默认选项

可以使用 QueryClient.setQueryDefaults() 方法为特定的 query 设置默认选项

tsx
queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })

function Component() {
  const { data } = useQuery(['posts'])
}
queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })

function Component() {
  const { data } = useQuery(['posts'])
}

为特定突变设置默认选项

可以使用 QueryClient.setMutationDefaults() 方法为特定的 mutation 设置默认选项

tsx
queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })

function Component() {
  const { mutate } = useMutation({ mutationKey: ['addPost'] })
}
queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })

function Component() {
  const { mutate } = useMutation({ mutationKey: ['addPost'] })
}

useIsFetching()

useIsFetching() Hook 现在接受过滤器,例如可以使用它仅为特定类型的 query 显示加载指示器

tsx
const fetches = useIsFetching({ queryKey: ['posts'] })
const fetches = useIsFetching({ queryKey: ['posts'] })

核心分离

React Query 的核心现在已完全从 React 中分离出来,这意味着它也可以独立使用或在其他框架中使用。使用 react-query/core 入口点仅导入核心功能

tsx
import { QueryClient } from 'react-query/core'
import { QueryClient } from 'react-query/core'

Devtools 现在是主仓库和 npm 包的一部分

devtools 现在包含在 react-query 包本身中,导入路径为 react-query/devtools。只需将 react-query-devtools 导入替换为 react-query/devtools