useQuery

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

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

演示

源码

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:
[]

用法

基础用法

组件挂载时,自动触发数据获取,同时返回数据、加载状态和错误信息,由 useAsyncFn 提供支持。

// fetchData 是请求库无关的,可以是任意返回 Promise 的异步函数,可由 fetch、axios、graphql 等实现 const { loading, data, error } = useQuery(fetchData) // 加载中、错误 UI 处理 if (loading) return <Loading /> if (error) return <Error /> // 渲染数据 return <div>{data}</div>

手动触发

当设置 manualtrue 时,所有内置的自动行为(默认都没开启,包括轮询加载、聚焦重加载、网路重连重加载)将失效。同时 immediate默认值被指定为 false

immediateuseMount 提供支持。

const { run, loading, data, error, params } = useQuery(fetchData, { manual: true, initialParams: ['params'], initialData: 'initialData', }) // 手动触发,会触发 params 更新 run('newParams')

依赖刷新

使用 refreshDependencies 可以设置刷新操作的依赖项,当依赖项改变时,将触发刷新 (refresh()) 操作,由 useUpdateEffect 提供支持。

const { loading, data, error } = useQuery(fetchData, { refreshDependencies: [dep1, dep2], })

操作取消

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

const { cancel, loading, data, error } = useQuery(fetchData) // 取消请求 cancel()

生命周期

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

const { loading, data, error } = useQuery(fetchData, { // 操作执行前 onBefore: (data, params) => console.log('before', data, params), // 操作执行后 onSuccess: (data, params) => console.log('success', data, params), // 操作失败 onError: (error, params) => console.log('error', error, params), // 操作完成 onFinally: (data, params) => console.log('finally', data, error, params), // 操作取消 onCancel: (data, params) => console.log('cancel', data, error, params), // 手动修改数据或参数 onMutate: (data, params) => console.log('mutate', data, params), // 刷新操作 onRefresh: (data, params) => console.log('refresh', data, params), })

慢加载状态

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

const { loading, loadingSlow, data, error } = useQuery(fetchData, { loadingTimeout: 3_000, // 默认为 0,这里设置为 3 秒 onLoadingSlow: () => console.log('loading slow'), // 慢加载时的回调 }) // 慢加载时显示慢加载 UI if (loading) return <Loading slow={loadingSlow} />

初始化和刷新中

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

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

const initializing = Boolean(!data && loading) const refreshing = Boolean(data && loading)
const { initializing, refreshing, data, error } = useQuery(fetchData) // 显示初始化中 UI if (initializing) return <Loading />; // 显示已有数据,同时根据 refreshing 状态来改变 UI 与其他样式 return <div>{data}{refreshing && <Loading />}</div>

参数和刷新

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

const { run, params, refresh } = useQuery(fetchData) // 获取上次请求的参数,存在 initialParams 时第一次为 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, // 在频繁触发的情况下,限制每秒只触发一次 // throttle: { wait: 1_000 }, // 或者指定整个 UseThrottledFnOptions 对象 debounce: 1_000, // 在频繁触发的情况下,等待操作停止的 1 秒后触发 // debounce: { wait: 1_000 }, // 或者指定整个 UseDebouncedFnOptions 对象 })

数据和参数修改

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

const { data, mutate } = useQuery(fetchData) // 更新数据 mutate('newData') // mutate((preData) => 'newData') // 支持 setState 风格的更新 // 同时更新数据和参数 mutate('newData', ['newPrams']) // 参数也支持 setState 风格的更新 // mutate((preData) => 'newData', (preParams) => ['newPrams']) // 修改全局缓存 import { mutate } from '@shined/react-use' mutate((key) => key === 'cacheKey', 'newData')

轮询加载

设置 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, refreshOnReconnect: true, refreshOnFocusThrottleWait: 3_000, // 聚焦刷新的节流时间,默认 5_000,仅 refreshOnFocus 为 true 时生效 })

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

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 时,如果也想控制内部自动行为,可以使用 Hooks 里对外暴露的 Pausable 实例属性,由 usePausable 提供支持。

const { pause, resume, isActive } = useQuery(fetchData, { refreshInterval: 5_000, // 指定每 5 秒刷新一次 }) // 暂停自动行为 pause() // 恢复自动行为 resume() // 判断是否处于活跃状态 console.log(isActive())

错误重试

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

const { loading, data, error } = useQuery(fetchData, { errorRetryCount: 3, // 错误重试次数,默认 0,关闭 errorRetryInterval: 1_000, // 错误重试间隔,默认 0,立即重试 onErrorRetry: (error) => console.log(error), // 错误重试时的回调 onErrorRetryFailed: (error) => console.log(error), // 错误重试失败时的回调 })

缓存与 SWR

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

const { loading, data, error } = useQuery(fetchData, { cacheKey: 'cacheKey', // 缓存键,可以是字符串或返回字符串的函数 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: 'cacheKey', }) // 组件 B const { data, params } = useQuery(fetchData, { cacheKey: 'cacheKey', }) // 在组件 A 中执行 refresh 或者 mutate 操作 refresh() mutate('newData', ['newParams']) // 在组件 B 里,data 和 params 会同步更新 console.log(data, params) // 'newData', ['newParams']

自定义数据更新

使用 compare 可以自定义数据更新逻辑,防止频繁刷新,这在处理某些伪不同的数据的情况下非常有用,比如时间戳,可以降低不必要的渲染,默认使用 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 提供支持。

// 所有状态变化都会触发重新渲染,不推荐!!! const fn = useQuery(fetchData) // 当 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, 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 = () => axios.get('https://api.example.com/data').then(res => res.data) // GraphQL 请求 const fetchData = () => graphqlClient.query({ query: gql`{ data }` }).then(res => res.data) // 自定义 Promise 函数 const fetchData = () => new Promise(resolve => setTimeout(() => resolve('data'), 1000)) // 可以抛出错误 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 => !data && loading) */ initializing: boolean /** * 请求是否正在刷新数据(有数据 + 加载中, refreshing => data && loading) */ refreshing: boolean }