feat: enhance useDataApi hook with default SWR options and improved refetch logic

- Introduced a centralized `DEFAULT_SWR_OPTIONS` object to standardize SWR configuration across the useDataApi hook, improving consistency and maintainability.
- Refactored refetch logic to utilize useCallback for better performance and to avoid stale closures.
- Updated mutation handling to use a ref for options, ensuring the latest options are always applied during API calls.
- Added a useEffect to reset pagination state when query parameters change, enhancing the hook's responsiveness to dynamic queries.
This commit is contained in:
fullex 2026-01-04 18:28:51 +08:00
parent 952e7c350b
commit 6a6f114946

View File

@ -5,7 +5,7 @@ import {
type OffsetPaginatedResponse,
type PaginatedResponse
} from '@shared/data/api/apiTypes'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { KeyedMutator } from 'swr'
import useSWR, { useSWRConfig } from 'swr'
import useSWRInfinite from 'swr/infinite'
@ -138,6 +138,17 @@ function getFetcher<TPath extends ConcreteApiPaths>([path, query]: [TPath, Recor
return apiFetcher(path, { query })
}
/**
* Default SWR configuration options shared across hooks
*/
const DEFAULT_SWR_OPTIONS = {
revalidateOnFocus: false,
revalidateOnReconnect: true,
dedupingInterval: 5000,
errorRetryCount: 3,
errorRetryInterval: 1000
} as const
/**
* Data fetching hook with SWR caching and revalidation
*
@ -159,17 +170,11 @@ export function useQuery<TPath extends ConcreteApiPaths>(
const key = options?.enabled !== false ? buildSWRKey(path, options?.query as Record<string, any>) : null
const { data, error, isLoading, isValidating, mutate } = useSWR(key, getFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: true,
dedupingInterval: 5000,
errorRetryCount: 3,
errorRetryInterval: 1000,
...DEFAULT_SWR_OPTIONS,
...options?.swrOptions
})
const refetch = () => {
mutate()
}
const refetch = useCallback(() => mutate(), [mutate])
return {
data,
@ -209,6 +214,12 @@ export function useMutation<TPath extends ConcreteApiPaths, TMethod extends 'POS
): UseMutationResult<TPath, TMethod> {
const { mutate: globalMutate } = useSWRConfig()
// Use ref to avoid stale closure issues with callbacks
const optionsRef = useRef(options)
useEffect(() => {
optionsRef.current = options
}, [options])
const apiFetcher = createApiFetcher(method)
const fetcher = async (
@ -229,25 +240,24 @@ export function useMutation<TPath extends ConcreteApiPaths, TMethod extends 'POS
populateCache: false,
revalidate: false,
onSuccess: async (data) => {
options?.onSuccess?.(data)
optionsRef.current?.onSuccess?.(data)
if (options?.revalidate === true) {
if (optionsRef.current?.revalidate === true) {
await globalMutate(() => true)
} else if (Array.isArray(options?.revalidate)) {
for (const path of options.revalidate) {
await globalMutate(path)
}
} else if (Array.isArray(optionsRef.current?.revalidate)) {
await Promise.all(optionsRef.current.revalidate.map((key) => globalMutate(key)))
}
},
onError: options?.onError
onError: (error) => optionsRef.current?.onError?.(error)
})
const optimisticMutate = async (data?: {
body?: BodyForPath<TPath, TMethod>
query?: QueryParamsForPath<TPath>
}): Promise<ResponseForPath<TPath, TMethod>> => {
if (options?.optimistic && options?.optimisticData) {
await globalMutate(path, options.optimisticData, false)
const opts = optionsRef.current
if (opts?.optimistic && opts?.optimisticData) {
await globalMutate(path, opts.optimisticData, false)
}
try {
@ -255,13 +265,13 @@ export function useMutation<TPath extends ConcreteApiPaths, TMethod extends 'POS
const result = await trigger(convertedData)
if (options?.optimistic) {
if (opts?.optimistic) {
await globalMutate(path)
}
return result
} catch (err) {
if (options?.optimistic && options?.optimisticData) {
if (opts?.optimistic && opts?.optimisticData) {
await globalMutate(path)
}
throw err
@ -278,7 +288,7 @@ export function useMutation<TPath extends ConcreteApiPaths, TMethod extends 'POS
}
return {
mutate: options?.optimistic ? optimisticMutate : normalMutate,
mutate: optionsRef.current?.optimistic ? optimisticMutate : normalMutate,
isLoading: isMutating,
error
}
@ -296,15 +306,14 @@ export function useMutation<TPath extends ConcreteApiPaths, TMethod extends 'POS
export function useInvalidateCache() {
const { mutate } = useSWRConfig()
const invalidate = (keys?: string | string[] | boolean): Promise<any> => {
const invalidate = async (keys?: string | string[] | boolean): Promise<void> => {
if (keys === true || keys === undefined) {
return mutate(() => true)
await mutate(() => true)
} else if (typeof keys === 'string') {
return mutate(keys)
await mutate(keys)
} else if (Array.isArray(keys)) {
return Promise.all(keys.map((key) => mutate(key)))
await Promise.all(keys.map((key) => mutate(key)))
}
return Promise.resolve()
}
return invalidate
@ -398,11 +407,7 @@ export function useInfiniteQuery<TPath extends ConcreteApiPaths>(
}
const swrResult = useSWRInfinite(getKey, infiniteFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: true,
dedupingInterval: 5000,
errorRetryCount: 3,
errorRetryInterval: 1000,
...DEFAULT_SWR_OPTIONS,
initialSize: 1,
revalidateAll: false,
revalidateFirstPage: true,
@ -469,6 +474,8 @@ export function usePaginatedQuery<TPath extends ConcreteApiPaths>(
query?: Omit<QueryParamsForPath<TPath>, 'page' | 'limit'>
/** Items per page (default: 10) */
limit?: number
/** Whether to enable the query (default: true) */
enabled?: boolean
/** SWR options */
swrOptions?: Parameters<typeof useSWR>[2]
}
@ -476,6 +483,12 @@ export function usePaginatedQuery<TPath extends ConcreteApiPaths>(
const [currentPage, setCurrentPage] = useState(1)
const limit = options?.limit || 10
// Reset page to 1 when query parameters change
const queryKey = JSON.stringify(options?.query)
useEffect(() => {
setCurrentPage(1)
}, [queryKey])
const queryWithPagination = {
...options?.query,
page: currentPage,
@ -484,6 +497,7 @@ export function usePaginatedQuery<TPath extends ConcreteApiPaths>(
const { data, isLoading, isRefreshing, error, refetch } = useQuery(path, {
query: queryWithPagination as QueryParamsForPath<TPath>,
enabled: options?.enabled,
swrOptions: options?.swrOptions
})