React Query 是 TanStack (Web开发的现代工具集)下的一个用于数据获取、缓存和同步的库,专为 React 应用而设。简化了应对数据获取的许多复杂性,内置了isPending、isFetching等状态方便了 UI 的渲染。

  1. 数据获取:提供了简化的数据获取机制,你只需专注于如何获取数据,而无需担心缓存、更新和错误处理等细节。
  2. 数据缓存:自动管理数据缓存,当数据过期时自动刷新,不再需要手动处理缓存逻辑。
  3. 后台数据更新:支持后台数据更新,确保用户始终看到最新的数据但不引入太多的请求开销。
  4. 错误处理:统一的错误处理机制,在数据获取失败时提供回调和重试策略。

安装

React Query 支持两种形式的安装,即通过包管理器安装和 CDN 引入:

NPM

$ npm i @tanstack/react-query
# or
$ pnpm add @tanstack/react-query
# or
$ yarn add @tanstack/react-query
# or
$ bun add @tanstack/react-query

CDN

<script type="module">
  import React from 'https://esm.sh/react@18.2.0'
  import ReactDOM from 'https://esm.sh/react-dom@18.2.0'
  import { QueryClient } from 'https://esm.sh/@tanstack/react-query'
</script>

devtools

如果想要使用官方的开发者工具进行可视化操作需要额外安装工具包

$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools
# or
$ bun add @tanstack/react-query-devtools

然后在入口文件处添加组件

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

组件可用的参数如下:

  • initiallsOpen:boolean,设置是否开启插件;
  • buttonPosition:string,按钮位置,可选值有”top-left” | “top-right” | “bottom-left”(默认) | “bottom-right” | “relative”,相对位置是靠近浏览器开发者面板的一侧;
  • position:string,面板位置,可选值”top” | “bottom”(默认) | “left” | “right”;
  • client:自定义QueryClient;
  • errorTypes:{ name: string; initializer: (query: Query) => TError},预定义查询中可能出现的错误,当出现该错误时执行初始化函数;
  • styleNonce:string,将随机数传给head,在开启CSP nonce时使用内联样式时使用;
  • shadowDOMTarget:ShadowRoot,如果指定shadowDOM会将内容渲染到shadowDOM中,默认渲染到页面内容的head中。

概念铺垫

再开始使用 React Query 之前有必要先了解一下他的一些概念,这将有助于理解 React Query 的工作方式:

queryKey

密钥,可以理解为查询的唯一ID,当 ID 相同时会共享缓存数据,,不同 queryKey 的查询之间不会互相干扰。queryKey 是一个数组,只要是可序列化的内容都可以作为 queryKey 的元素。!!!但是要注意,数组的顺序很关键,顺序不一致会被判定为不一致,如果元素是对象,则不必关注顺序,只要KV重合就可以判定一致。

缓存

上面说过了 queryKey 主要用于缓存,紧接着来看下缓存机制。首次查询会根据 queryKey 进行缓存,默认为5分钟,在有效期时间(staleTime)内,有相同 queryKey 的查询会直接返回缓存内容,不会触发真实的请求,如果缓存过期(默认5分钟)会重新请求。缓存的数据会在一段时间(cacheTime)后删除。staleTime 和 cacheTime 都可以手动控制。

重试

重试,当使用 useQuery 查询失败时会进行指定次数的重试,默认3次,可以在option中单独指定。false——不进行重试,true——无线重试,number——指定重试次数,function——根据失败原因自定义逻辑。重试会有延迟时间,默认1s,每次重试都会将延迟时间加倍(上限30s),可以通过retryDelay 设置等待时间,手动设置的等待时间会固定生效。

查询和突变

React Query 中存在两种请求模式——查询和突变,查询的意思是获取服务端数据而不对服务端数据产生影响的请求,突变即为可能导致服务端数据改变(新增、修改和删除数据)的请求

状态

React Query 提供了一系列属性来描述查询的状态

  • status:查询的状态,可用于根据查询状态进行不同场景的渲染,比如区分渲染加载中、空数据、有数据三种情况会很方便,根据status的值衍生出了其他几个属性
    • isPending:status==='pending',查询还没有数据;
    • isError:status==='error',查询出现错误;
    • isSuccess:status==='success',查询成功并且数据可用。
  • fetchStatus:请求的状态,标识查询所触发的真实网络请求的状态,如果需要UI体现出网络请求将会非常实用,同样fetchStatus也衍生出了几个属性
    • isFetching:fetchStatus==='fetching,查询正在进行请求;
    • isPaused:fetchStatus==='paused,查询想要发起请求,但是被暂停;
    • 还有一种状态没有单独设置变量fetchStatus==='idle,查询此时没有进行任何处理。

常用hooks

React Query 提供了一系列 hooks 供使用,下面我们以社区文章为场景简单的进行一下示例各个 hooks 的使用场景。

useQuery

useQuery 算是 React Query 中最核心的 hooks 之一了,用于请求服务器的数据并缓存。除了前文讲的useQuery需要传入 queryKey 之外,还要穿入一个 queryFn 方法用于请求数据,另外支持对每个查询进行自定义配置,默认的配置如下:

  • staleTiem:重新获取数据的时间间隔,默认为0

  • cacheTime:数据缓存时间,默认5分钟

  • retry:失败重试次数,默认3次;

  • refetchOnWindowFocus:窗口重新获得焦点时重新获取数据,默认为false

  • refetechOnReconnect:网络重新链接

  • refetchOnMount:实例重新挂载时重新拉取请求

  • enabled:如果为false,useQuery不会触发,需要使用其返回的refetch来触发操作

返回一个经过包装的对象,data 为请求到的数据,另外携带了之前讲的请求状态、refetch等。

在文章列表,我们可以通过 useQuery 来查询数据,然后根据状态和数据等信息进行渲染

export default function Query() {
  const { data, isPending } = useQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
    staleTime: 1000 * 60,
  })

  return (
    <Spin spinning={isPending}>
      <List
        header={
          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <h3>文章列表</h3>
            <Button
              type="primary"
              onClick={() => history.push('/query/create')}
            >
              写文章
            </Button>
          </div>
        }
        dataSource={data}
        renderItem={item => <List.Item>
          <a
            onClick={() => history.push(`/query/${item.id}`)}
          >
            {item.title}
          </a>
        </List.Item>
        }
      />
    </Spin>
  )
}

Tips: 当父子组件用到同一接口返回的数据时,可以在父子组件中分别使用 useQuery,但是只会发起一次请求,这样就会减少很多非必要的 props,使得组件更加的简洁。

此时打开 React Query devtools 可以看到我们刚才请求的数据被记录下来了,并且devtools提供了很多操作供开发者调试,比如:模拟加载中、失败等状态;手动失效缓存数据;手动触发请求;网络状态控制等等

image-20240707204230401

当超过缓存的新鲜时间时,缓存状态状态会变为stale

image-20240707204343926

如果查询依赖一些异步参数之类不能初始化获取的变量,可以通过 enabled 来控制,比如在详情页,我们依赖于 postId 来查询详情(这里的postId是uel解析出来的,可以立刻取到,仅提供一个enabled的使用思路)

export default function Detail() {
  const { id } = useParams()
  const { data, isPending } = useQuery({
    queryKey: ['post', id],
    queryFn: () => getPostBtyId(Number(id)),
    staleTime: 1000 * 60 * 5,
    enabled: !!id,
  })

  return (
    <Spin spinning={isPending}>
      <h3>
        <ArrowLeftOutlined
          onClick={history.back}
          style={{ marginRight: 16 }}
        />
        {data?.title}
      </h3>

      <article>
        {data?.content}
      </article>
    </Spin>
  )
}

useMutation

前文提到,当涉及到服务器数据变动时应该使用 useMutaion。和 useQuery 不同的是,useQuery 是自动发起的因为不涉及服务端数据变动,查询在任何时机执行多少次都无所谓,但是 mutation 不同,需要在特定的时机触发,比如表单填写完成点击提交。

所以 useMutation 返回了一个 mutation 对象,通过 mutate 方法来执行操作,例如通过 id 删除文章。可以设置onSuccess和onError来预设请求成功或者失败后执行的回调,除了一般常用的操作之外,还可以用来重置缓存的有效期。

export default function Query() {
  // ...省略已有代码
  const { mutate } = useMutation({
    mutationFn: (id: number) => deletePost(id),
    onSuccess: () => {
      message.success('删除成功')
      refetch()
    },
  })

  return (
    <Spin spinning={isPending}>
      <List
        // ...
        dataSource={data}
        renderItem={item => <List.Item
          extra={
            <Popconfirm
              title="确定删除吗?"
              description="删除后不可恢复!"
              onConfirm={() => mutate(Number(item.id))}
              okText="确定"
              cancelText="取消"
            >
              <DeleteOutlined style={{ color: 'red' }} />
            </Popconfirm>
          }
        >
          <a
            onClick={() => history.push(`/query/${item.id}`)}
          >
            {item.title}
          </a>
        </List.Item>
        }
      />
    </Spin>
  )
}

mutate 可以将mutationFn的结果返回,如果是异步方法,可以使用mutateAsync方法。

useQueryClient

通过useQueryClient,我们可以获取到之前注入的实例,里面保存着所有我们缓存的信息,以及配置信息。获取实例后我们可以缓存等进行操作,也就是上面 useMutation 提到的手动失效缓存。

  • getQueryCache():包含了所有当前已经client缓存的查询键和相应的查询结果;

  • fetchQuery(queryKey,queryFn,options?):用于获取和缓存查询,要么与数据一起解析,要么与错误一起抛出;

  • fetchInfiniteQuery(queryKey,queryFn,options?):类似于fetchQuery,但可用于获取和缓存无限查询。

  • prefetchQuery(queryKey,queryFn,options?):数据预获取,并将结果存储在缓存中,以便稍后使用

  • prefetchInfiniteQuery(queryKey,queryFn,options?):类似于prefetchQuery,但可用于获取和缓存无限查询。

  • getQueryData(queryKey):根据指定的查询键从缓存中获取数据;

  • setQueryData(queryKey,data,options?):将指定数据存储到缓存中,并触发相应更新;

  • invalidateQueries((key)=>key.startsWith(‘examplae))

    invalidateQueries(queryKey,options?):使特定查询失效,下次访问时会重新获取最新数据;

  • refetchQueries(queryKeyOrPredicateFn,options?):手动触发重新获取指定或匹配条件的查询数据。

  • cancelQueries(queryKey,options?):取消正在进行中的查询请求,避免不必要的网络请求

  • removeQueries(queryFilter):通过queryKey或者其他查询条件来移除缓存,比如removeQueries({ queryKey: ['posts'], type: 'inactive' })

  • resetQueries(queryFilter):通过queryKey或者其他查询条件来重置查询;

  • clear():清除整个queryClient内容,包括所有的queries和mutations数据

比如以下场景,当发布一个新的文章之后导航到列表页,可能会命中缓存这样就看不到新的数据,此时我们就可以通过invalidateQueries来使列表的缓存失效,以实现重新请求

export default function Create() {
  const client = useQueryClient()
  const { mutateAsync } = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      message.success('发布成功')
      client.invalidateQueries({ queryKey: ['posts'] })
      history.push('/query')
    }
  })
  
  return (
    <Form
      layout="vertical"
      onFinish={mutateAsync}
    >
      <Form.Item label="标题" name="title">
        <Input />
      </Form.Item>

      <Form.Item label="内容" name="content">
        <Input.TextArea />
      </Form.Item>

      <Space>
        <Button type="primary" htmlType="submit">
          发布
        </Button>

        <Button
          onClick={() => history.push('/query')}
        >
          取消
        </Button>
      </Space>
    </Form>
  )
}

此外,还可以通过 queryClient 实现乐观 UI优化,当 mutate 动作触发后立刻对缓存数据进行更新,如果请求失败利用快照进行还原,可以优化用户在网络 IO的体验,将操作延迟的感知降低。

当文章需要修改时,触发发布动作的时候可以将数据更新到list的缓存中,如果请求失败则进行还原。

const queryOptions = {
  queryKey: ['posts'],
  queryFn: getPosts
}

export default function Update() {
  const client = useQueryClient()
  const { mutateAsync } = useMutation({
    mutationFn: (post: IPost) => updatePost(post.id as number, post),
    onMutate: async (edited) => {
      // 读取缓存
      client.cancelQueries(queryOptions)
      // 查找位置
      const cache = client.getQueryData(queryOptions.queryKey) as IPost[]
      if (cache) {
        // 替换数据
        const target = cache.findIndex(item => item.id === edited.id)
        cache.splice(target, 1, edited)
        client.setQueryData(queryOptions.queryKey, [...cache])
      }

      return { cache }
    },
    onSuccess: () => {
      message.success('更新成功')
      history.push('/query')
    },
    // 失败后将缓存设置为原来的值
    onError: (err, variables, context) => {
      if (context?.cache) {
        client.setQueryData<IPost[]>(queryOptions.queryKey, context.cache)
      }
    },
    // 会在失败或者成功后执行,用于更新真实的数据
    onSettled: () => {
      client.invalidateQueries({ queryKey: queryOptions.queryKey })
    }
  })
  const { id } = useParams()
  const { data } = useQuery({
    queryKey: ['post', id],
    queryFn: () => getPostBtyId(Number(id)),
    staleTime: 1000 * 60 * 5,
    enabled: !!id,
  })

  const [form] = Form.useForm<IPost>()
  useEffect(() => {
    if (data) {
      form.setFieldsValue(data)
    }
  }, [data])

  return (
    <Form
      form={form}
      layout="vertical"
      onFinish={(values) => mutateAsync({ id: data?.id, ...values })}
    >
      <Form.Item label="标题" name="title">
        <Input />
      </Form.Item>

      <Form.Item label="内容" name="content">
        <Input.TextArea />
      </Form.Item>

      <Space>
        <Button type="primary" htmlType="submit">
          更新
        </Button>

        <Button
          onClick={() => history.push('/query')}
        >
          取消
        </Button>
      </Space>
    </Form>
  )
}

useQueries

React Query 中的 useQueries 是一个用于同时发起多个查询的钩子函数。通过将多个查询的键和配置对象传递给 useQueries,可以在组件中一次性启动并管理多个独立的数据获取请求。

useQueries 返回一个数组,包含每个查询的状态、数据和方法。这样可以更方便地对不同的异步操作进行处理,并能够实时更新 UI 以反映最新状态。

如果页面上的数据依赖多个接口,并且希望内容同时渲染时,可以使用useQueries进行请求。

const ids = [1, 2, 3]
const results = useQueries({
  queries: ids.map((id) => ({
    queryKey: ['post', id],
    queryFn: () => getPostById(id),
    staleTime: 1000 * 60 * 5,
  })),
})

如果想要对结果进行自定义组合,可以使用combine

const ids = [1, 2, 3]
const combinedQueries = useQueries({
  queries: ids.map((id) => ({
    queryKey: ['post', id],
    queryFn: () => getPostById(id),
    staleTime: 1000 * 60 * 5,
  })),
  combine: (results) => {
    return {
      data: results.map((result) => result.data),
      pending: results.some((result) => result.isPending),
    }
  },
})

useQueries 也可以进行依赖控制,通过条件控制 queries 数组内容即可,当不需要发起请求时设为空数组

const { data: userIds } = useQuery({
  queryKey: ['users'],
  queryFn: getUsersData,
  select: (users) => users.map((user) => user.id),
})

const usersInfo = useQueries({
  queries: userIds
    ? userIds.map((id) => {
        return {
          queryKey: ['usersInfo', id],
          queryFn: () => getInfoByUserId(id),
        }
      })
    : [],
})

前端小白