useQuery

这个 Hook 自 v1.5.0 版本起可用。

一个基础的 React 钩子,用于数据获取,支持自动刷新、错误重试、缓存以及许多其他强大功能。

场景

  • 数据获取: 绝大多数的数据获取场景,支持任意自定义异步函数作为 Fetcher
  • 刷新、错误重试、缓存: 需要自动刷新、错误重试、缓存等功能的场景
  • 限制频率、依赖刷新: 需要控制数据获取的限制频率、支持依赖刷新等场景

演示

源码

Immediate + Trigger by user + Dependencies

Data:
Loading...

Lifecycle + Refresh + Params + Mutate + Cancel

Data:
Not loaded
Params:
[]

Throttle + Debounce

Data with debounce:
Not loaded
Data with throttle:
Not loaded

ReFocus + ReConnect + AutoRefresh + Loading Slow

Data:
Initializing...

Error Retry + Cache (SWR)

Data:
Not loaded
Params:
[]
Data2:
Not loaded
Params2:
[]

用法

基础用法

组件挂载时,会自动触发数据获取(immediate 选项默认值为 true),同时返回数据、加载状态和错误信息,由 useAsyncFn 提供支持。

// 这里的 `fetchData` 被叫做 `Fetcher`,是一个请求库无关、返回任意 Promise 的异步函数
// Fetcher 函数可以通过 fetch、axios、graphql 等方式进行自定义实现
const { loading, data, error } = useQuery(fetchData)

// 加载中、错误 UI 处理
if (loading) return <Loading />
if (error) return <Error />

// 渲染数据
return <div>{data}</div>

手动触发

  • 如果不需要组件挂载时自动获取数据,可以设置 immediate 选项为 false 来禁用。

  • 如果需要禁用所有内置自动行为(包括轮询加载聚焦重加载网络重连重加载等,均默认关闭),可以设置 manual 选项为 true,此时 immediate 的缺省默认值也将被指定为 false

immediateuseMount 提供支持。不需要首次触发首选 immediate 选项,manual 选项通常用于调试等场景。

const { run, loading, data, error, params } = useQuery(fetchData, {
  manual: true, // 是否手动执行,默认为 false,即会触发自动行为
  // initialParams: ['params'], // 初始参数,默认为 []
  // initialData: 'initialData', // 初始数据,默认为 undefined
})

// 需要手动通过 run 函数触发,内部自动维护 loading、data、params 等状态
run('newParams')

依赖刷新

使用 refreshDependencies 可以设置刷新操作的依赖项,当依赖项改变时,将触发刷新 (refresh()) 操作,由 useUpdateEffect 提供支持,默认只会进行浅比较,与 useEffect 依赖比较的默认行为保持一致。

const { loading, data, error } = useQuery(fetchData, {
  refreshDependencies: [dep1, dep2], // 自动刷新的依赖项,默认为 []
})

// 进行 setDep1('newDep1') 或 setDep2('newDep2') 操作时,将触发刷新操作

操作取消

执行返回的 cancel 函数可以取消当前请求,同时重置所有状态。请注意,cancel 无法阻止 Promise 执行,它只是终止了后续状态更新逻辑,由 useAsyncFn 提供支持

const { cancel, loading, data, error } = useQuery(fetchData)

// 取消请求
cancel()

生命周期

useQuery 提供了丰富的生命周期,由 useAsyncFn 提供支持,包括:

const { loading, data, error } = useQuery(fetchData, {
  // 在操作执行前,比如进行乐观 UI 更新
  onBefore: (data, params) => console.log('before', data, params),
  // 在操作成功时
  onSuccess: (data, params) => console.log('success', data, params),
  // 在操作失败时,可以捕获错误、回退 UI 等
  onError: (error, params) => console.log('error', error, params),
  // 在操作结束时(无论成功或失败)
  onFinally: (data, params) => console.log('finally', data, params),
  // 在操作被取消时
  onCancel: (data, params) => console.log('cancel', data, params),
  // 在手动修改数据或参数时
  onMutate: (data, params) => console.log('mutate', data, params),
  // 在进行刷新操作时
  onRefresh: (data, params) => console.log('refresh', data, params),
})

这里需要注意的是,在操作失败时,onError 会捕获错误,以供用于错误提示、错误上报等。如果你期望它直接抛出错误,可以在这里 throw,但是请注意抛出的错误会阻止 error 状态更新,以及后续 onFinally 的执行。

const { loading, data, error } = useQuery(fetchData, {
  onError: (error) => {
    console.log('error', error)
    throw error // 抛出错误,但这会阻止 error 状态按预期更新,以及后续 `onFinally` 的执行。
  },
})

慢加载状态

使用 loadingSlow 可以获取当前操作是否处于慢加载状态,即操作时间(一般是网络请求耗时)超过预期阈值,通过这个状态条件渲染不同的 UI 可以改善用户体验,由 useLoadingSlowFn 提供支持。

const { loading, loadingSlow, data, error } = useQuery(fetchData, {
  loadingTimeout: 3_000, // 慢加载阈值,默认为 0,这里设置为 3 秒
  onLoadingSlow: () => console.log('loading slow'), // 处于慢加载时的回调函数
})

// 加载时显示加载 UI,并在慢加载时额外显示慢加载 UI
if (loading) return <Loading slow={loadingSlow} />

初始化和刷新中

使用 initializingrefreshing 可以获取当前是否处于初始化和刷新中。

const { initializing, refreshing, data, error } = useQuery(fetchData)

// 显示初始化中 UI
if (initializing) return <Initializing />;

// 显示已有数据,同时根据 refreshing 状态来改变 UI 与其他样式
return (
  <div className={refreshing ? 'opacity-60' : ''}>
    {data}
    {refreshing && <Loading />}
  </div>
)

这两个状态其实是 loadingdata 的衍生状态,以下是伪代码:

const initializing = Boolean(!data && loading)
const refreshing = Boolean(data && loading)

参数和刷新

使用 params 可以获取上次请求的参数,使用 refresh 可以使用上次请求的参数重新请求,由 useAsyncFn 提供支持。

const { run, params, refresh } = useQuery(fetchData)

// 获取上次请求的参数,如果存在 initialParams,则第一次为即为它,否则为 []
console.log(params)
// 使用上一次的参数重新请求,即「刷新」,等价于 `run(params)`
refresh()

自动清空与取消

使用 clearBeforeRun 可以在每次请求前清空数据,使用 cancelOnUnmount 可以在组件卸载时自动取消请求逻辑(但无法阻止 Promise 执行),由 useAsyncFn 提供支持。

const { loading, data, error } = useQuery(fetchData, {
  clearBeforeRun: true, // 是否在请求前清空数据,默认 false,即不清空
  cancelOnUnmount: false, // 是否在组件卸载时取消请求,默认 true,即自动取消
})

防抖和节流

使用 throttledebounce 选项可以控制手动触发的频率,由 useThrottledFnuseDebouncedFn 提供支持。

const { run, loading, data, error } = useQuery(fetchData, {
  throttle: 1_000, // 节流等待值,默认为 0,不开启,这里设置在频繁触发的情况下,限制每秒只触发一次
  // throttle: { wait: 1_000 }, // 也可以指定为整个 UseThrottledFnOptions 对象

  debounce: 1_000, // 防抖等待值,默认为 0,不开启,这里设置在频繁触发的情况下,等待操作停止的 1 秒后触发
  // debounce: { wait: 1_000 }, // 也可以指定为整个 UseDebouncedFnOptions 对象
})

数据和参数修改

使用 mutate 可以直接修改数据和请求参数并同时触发重新渲染,不会触发额外的请求,由 useAsyncFn 提供支持。

const { data, mutate } = useQuery(fetchData)

// 更新数据
mutate('newData')

// 支持 setState 风格的更新
// mutate((preData) => 'newData')

// 同时更新数据和参数
mutate('newData', ['newPrams'])

// 参数也支持 setState 风格的更新
// mutate((preData) => 'newData', (preParams) => ['newPrams'])

// 修改全局缓存,仅对默认的全局 Map provider 有效,自定义 provider 需自行处理
import { mutate } from '@shined/react-use'
mutate((key) => key === 'cacheKey', 'newData', ['newParams'])

轮询加载

设置 refreshInterval 为一个大于 0 的数字,将启用自动刷新功能,每隔指定时间重新获取数据,由 useIntervalFn 提供支持。

const { loading, data, error } = useQuery(fetchData, {
  refreshInterval: 5_000, // 轮询时间间隔,默认 0,不启用
  refreshWhenHidden: true, // 是否在页面不可见时轮询,默认 false
  refreshWhenOffline: true, // 是否在离线时轮询,默认 false
})

重聚焦和重连重加载

设置 refreshOnFocusrefreshOnReconnecttrue,将在页面聚焦和网络重连时重新获取数据,由 useReFocusFnuseReConnectFn 提供支持。

const { loading, data, error } = useQuery(fetchData, {
  refreshOnFocus: true, // 聚焦时是否重新加载,默认 false
  refreshOnReconnect: true, // 网络重连时是否重新加载,默认 false
  refreshOnFocusThrottleWait: 3_000, // 聚焦刷新的节流等待时间,默认 5_000
})

如果需要在 React NativeInk非浏览器环境中使用,可以手动指定相关的可见性和网络状况的判断逻辑。

import { createReactNativeReFocusRegister } from '@shined/react-use'
import { AppState } from 'react-native'

const { loading, data, error } = useQuery(fetchData, {
  isVisible: () => true, // 自定义可见性判断函数
  isOnline: () => true, // 自定义是否在线判断函数
  registerReConnect: createReactNativeReFocusRegister(AppState), // 内置了 React Native 的网络重连注册函数
  registerReFocus: (callback) => {}, // 自定义聚焦事件的注册函数
})

可暂停 (Pasuable)

当没有显式地指定 manualfalse 时,如果也想控制内部自动行为,可以使用对外暴露的 Pausable 实例,由 usePausable 提供支持。

const { pause, resume, isActive } = useQuery(fetchData, {
  refreshInterval: 5_000, // 指定每 5 秒刷新一次
})

// 暂停自动行为,这里只有「轮询刷新」这一个自动行为
pause() 

// 恢复自动行为,这里只有「轮询刷新」这一个自动行为
resume()

// 判断当前配置的自动行为是否处于活跃状态
console.log(isActive())

错误重试

设置 errorRetryCount 为大于 0 的数字,将在请求失败时自动重试,由 useRetryFn 提供支持。

const { loading, data, error } = useQuery(fetchData, {
  errorRetryCount: 3, // 错误重试次数,默认 0,关闭
  errorRetryInterval: 1_000, // 错误重试间隔,默认为 `useRetryFn` 中内置的退避算法
  onErrorRetry: (error) => console.log(error), // 尝试重试时的回调
  onErrorRetryFailed: (error) => console.log(error), // 错误重试失败时的回调
})

缓存与 SWR

设置 cacheKey 可以启用缓存功能,缓存的内容包括 dataparam,会优先返回缓存数据,当存在缓存数据且未过期时(过期会被清空)触发请求,并在请求结果返回时更新缓存数据,以保证数据可用性和最新性,也就是 SWR(Stale-While-Revalidate)策略。

const { loading, data, error } = useQuery(fetchData, {
  cacheKey: 'cacheKey', // 缓存 key,可以是字符串或返回字符串的函数
  cacheExpirationTime: 5 * 60 * 1000, // 最大缓存时间,默认 5 分钟,设置 `false` 以阻止过期,即永久缓存
})

可以指定 provider 为一个外部存储(如 Reactive)、localStorage 等,以实现多处共享缓存或者更加精细化的局部缓存。一个 Provider 需要符合以下接口定义(基本上就是符合 Map 类型接口的对象):

export interface UseQueryCacheLike<Data> {
  get(key: string): Data | undefined
  set(key: string, value: Data): void
  delete(key: string): void
  keys(): IterableIterator<string>
}

比如某些部分使用独立的 Map 缓存来共享数据:

const cache = new Map<string, any>()

// 组件 A
const { loading, data, error } = useQuery(fetchData, {
  cacheKey: 'cacheKeyA', 
  provider: cache, // 使用独立的 Map 作为缓存提供者
})

// 组件 B
const { loading, data, error } = useQuery(fetchData, {
  cacheKey: 'cacheKeyB', 
  provider: cache, // 使用独立的 Map 作为缓存提供者
})

// 组件 C, 虽然 cacheKey 与 A 一样,但是 provider 不一样,所以不会共享缓存
const { loading, data, error } = useQuery(fetchData, {
  cacheKey: 'cacheKeyA', // 不指定 provider,使用默认的全局共享 Map
})

或者使用 localStorage 作为缓存提供者,以实现页面刷新后的数据持久化,但请注意数据的序列化和反序列化,以及数据的大小限制是否符合需求。

const localStorageProvider = {
  get: (key: string) => {
    const value = localStorage.getItem(key)
    return value ? JSON.parse(value) : undefined
  },
  set: (key: string, value: string) => localStorage.setItem(key, JSON.stringify(value)),
  delete: (key: string) => localStorage.removeItem(key),
  keys: () => Object.keys(localStorage)[Symbol.iterator](),
}

const { loading, data, error } = useQuery(fetchData, {
  cacheKey: 'cacheKey',
  provider: localStorageProvider, // 使用 localStorage 作为缓存提供者
})

如果 cacheKeyprovider 均相同,则其会被认为是同一个缓存,同一缓存的任一处状态、数据的变更,都会自动同步到其他处,常见于多个组件使用同一份数据的情况。

例如用户信息,可能在 nav 组件、header 组件、sidebar 组件等多处被使用,这时候可以使用 cacheKeyprovider 来实现数据的共享,以实现数据的同步更新。

// 组件 A
const { loading, data, mutate, refresh } = useQuery(fetchData, {
  cacheKey: 'sameCacheKey',
})

// 组件 B
const { data, params } = useQuery(fetchData, {
  cacheKey: 'sameCacheKey',
})

// 在组件 A 中执行 refresh 或者 mutate 操作
refresh()
mutate('newData', ['newParams'])

// 在组件 B 里,data 和 params 会同步更新
console.log(data, params) // 'newData', ['newParams']

自定义数据更新

使用 compare 可以自定义数据更新逻辑,防止频繁刷新,这在处理某些伪变化数据的情况下非常有用,这可以降低不必要的渲染。例如请求返回的 body 只是时间戳 (timestamp) 改变,但实际数据 (data) 不变的情况。

默认使用 shallowEqual,即只比较一层,由 useAsyncFn 提供支持。

const { loading, data, error } = useQuery(fetchData, {
  compare: (preData, nextData) => {
    // 比较数据是否严格相同,返回 true 则不更新,返回 false 则更新
    // return preData === nextData

    // 只比较数据的某个字段,忽略干扰字段,比如 timestamp
    return deepCompare(preData?.data, nextData?.data)

    // 但请注意,数据量太大时,深比较会影响性能,请权衡使用
  },
})

依赖收集

useQuery 实现了依赖收集策略,实现了按需渲染,最大限度优化性能,由内部的 useTrackedRefState 提供支持。

// 当 loading、data、error 状态变化时,都会触发重新渲染
const { loading, data, error } = useQuery(fetchData)

// 仅当 loading 状态变化时,才会触发重新渲染,data 和 error 状态变化不会触发,因为没有用到
const { loading } = useQuery(fetchData)

// 所有支持依赖收集的状态属性
const { loading, data, error, params, loadingSlow, initializing, refreshing } = useQuery(fetchData)

有关其背景、实现原理、细节等,请参考 依赖收集

源码

点击下方链接跳转 GitHub 查看源代码。

API

const {
  run, data, loading, refreshing, initializing, error,
  cancel, params, refresh, mutate, loadingSlow, ...pausable
} = useQuery(fetcher, options)

数据获取函数 Fetcher

一个异步函数,用于数据获取,返回一个 Promise,与请求库无关。例如以下都是有效的 Fetcher 函数:

// 原生 Fetch 请求
const fetchData = async () => await (await fetch('https://api.example.com/data').json())
// Axios 请求
const fetchData = async () => (await axios.get('https://api.example.com/data')).data
// GraphQL 请求
const fetchData = () => (await graphqlClient.query({ query: gql`{ data }` })).data
// 自定义 Promise 函数
const fetchData = () => new Promise(resolve => setTimeout(() => resolve('data'), 1000))
// 可以抛出错误,会被 `onError` 捕获,可通过返回的 `error` 属性控制 UI
const fetchData = () => new Promise((_, reject) => setTimeout(() => reject('error'), 1000))

选项 Options

有关更多详情,请查看 UseLoadingSlowFnOptions, UseReConnectOptions, UseReFocusOptions, UseThrottledFnOptions, UseDebouncedFnOptions, UseIntervalFnInterval and UseRetryFnOptions

export interface UseQueryOptions<T extends AnyFunc, D = Awaited<ReturnType<T>>, E = any>
  extends Omit<UseLoadingSlowFnOptions<T, D, E>, 'initialValue'>,
    Pick<UseReConnectOptions, 'registerReConnect'>,
    Pick<UseReFocusOptions, 'registerReFocus'> {
  /**
   * 禁用所有自动刷新行为, 默认关闭
   *
   * @defaultValue false
   */
  manual?: boolean
  /**
   * 初始挂载时传递给提取器的数据
   *
   * @defaultValue undefined
   */
  initialData?: D | undefined
  /**
   * 缓存键,可以是字符串或返回字符串的函数
   *
   * @defaultValue undefined
   */
  cacheKey?: string | ((...args: Parameters<T> | []) => string)
  /**
   * 最大缓存时间,指定时间后清除缓存
   *
   * 默认为 5 分钟,设置 `false` 以禁用
   */
  cacheExpirationTime?: number | false
  /**
   * 缓存提供者,可以设置为外部存储(响应式)、localStorage 等。
   *
   * 需要符合 CacheLike 接口定义,默认为全局共享的 `new Map()`
   *
   * @defaultValue global shared `new Map()`
   */
  provider?: Gettable<CacheLike<D>>
  /**
   * 节流选项 => 仅影响手动执行 run 方法的频率
   *
   * @defaultValue undefined
   */
  throttle?: UseThrottledFnOptions['wait'] | UseThrottledFnOptions
  /**
   * 防抖选项 => 仅影响手动执行 run 方法的频率
   *
   * @defaultValue undefined
   */
  debounce?: UseDebouncedFnOptions['wait'] | UseDebouncedFnOptions
  /**
   * 获取焦点时是否重新加载, 默认关闭
   *
   * @defaultValue false
   */
  refreshOnFocus?: boolean
  /**
   * 获取焦点时的节流时间,默认 5_000 (毫秒),只有在 refreshOnFocus 为 true 时生效
   *
   * @defaultValue 5_000
   */
  refreshOnFocusThrottleWait?: number
  /**
   * 自定义可见性判断函数
   *
   * @defaultValue defaultIsVisible
   */
  isVisible?: () => Promisable<boolean>
  /**
   * 网络重连时是否重新加载, 默认关闭
   *
   * @defaultValue false
   */
  refreshOnReconnect?: boolean
  /**
   * 自定义在线判断函数
   *
   * @defaultValue defaultIsOnline
   */
  isOnline?: () => Promisable<boolean>
  /**
   * 自动刷新的间隔时间,默认为 0,关闭
   *
   * @defaultValue 0
   */
  refreshInterval?: Exclude<UseIntervalFnInterval, 'requestAnimationFrame'>
  /**
   * 隐藏时是否重新加载,默认关闭
   *
   * @defaultValue false
   */
  refreshWhenHidden?: boolean
  /**
   * 离线时是否重新加载,默认关闭
   *
   * @defaultValue false
   */
  refreshWhenOffline?: boolean
  /**
   * 刷新操作的依赖项,当依赖项改变时,将触发刷新操作
   *
   * @defaultValue []
   */
  refreshDependencies?: DependencyList
  /**
   * 错误重试次数
   *
   * @defaultValue 0
   */
  errorRetryCount?: UseRetryFnOptions<E>['count']
  /**
   * 错误重试间隔
   *
   * @defaultValue 0
   */
  errorRetryInterval?: UseRetryFnOptions<E>['interval']
  /**
   * 是否在每次请求前清除缓存
   *
   * @defaultValue undefined
   */
  onErrorRetry?: UseRetryFnOptions<E>['onErrorRetry']
  /**
   * 错误重试失败时的回调
   * 
   * @defaultValue undefined
   */
  onErrorRetryFailed?: UseRetryFnOptions<E>['onRetryFailed']
}

返回值

返回值中包含可暂停、恢复的 Pausable 实例。

更多详情,请参见 Pausable

有关更多详情,请查看 UseLoadingSlowFnReturns

export interface UseQueryReturns<T extends AnyFunc, D = Awaited<ReturnType<T>>, E = any>
  extends Pausable,
    Omit<UseLoadingSlowFnReturns<T, D, E>, 'value'> {
  /**
   * 请求返回的数据
   */
  data: D | undefined
  /**
   * 请求是否处于初始化状态,无数据 + 加载中, initializing => Boolean(!data && loading)
   */
  initializing: boolean
  /**
   * 请求是否正在刷新数据,有数据 + 加载中, refreshing => Boolean(data && loading)
   */
  refreshing: boolean
}