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>

手动触发

当设置 manual 选项为 true 时,useQuery 的所有内置自动行为(这些行为包括轮询加载聚焦重加载网络重连重加载等,均默认关闭)将失效,同时 immediate 的缺省默认值被指定为 false

immediateuseMount 提供支持。

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 }