import { noop, notifyManager } from '@tanstack/query-core'
import { useCallback, useEffect, useState } from 'preact/hooks'
import { useIsRestoring } from './IsRestoringProvider'
import { useQueryClient } from './QueryClientProvider'
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
import {
ensurePreventErrorBoundaryRetry,
getHasError,
useClearResetErrorBoundary,
} from './errorBoundaryUtils'
import {
ensureSuspenseTimers,
fetchOptimistic,
shouldSuspend,
} from './suspense'
import { useSyncExternalStore } from './utils'
import type { UseBaseQueryOptions } from './types'
import type {
QueryClient,
QueryKey,
QueryObserver,
QueryObserverResult,
} from '@tanstack/query-core'
* Base implementation shared by `useQuery`, `useInfiniteQuery`, `useSuspenseQuery`, and
* `useSuspenseInfiniteQuery`.
* @param options - The options passed to the calling hook.
* @param Observer - The observer class from query-core (`QueryObserver` or
* `InfiniteQueryObserver`).
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest
* context will be used.
* @returns The query result, tracking which properties are read unless `notifyOnChangeProps` is
* set.
* @throws {Error} If `options` is not an object (outside production), or the query error when it should be
* thrown to the nearest error boundary (see `throwOnError`). While suspending, it throws a promise instead.
*/
export function useBaseQuery<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey extends QueryKey,
>(
options: UseBaseQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
Observer: typeof QueryObserver,
queryClient?: QueryClient,
): QueryObserverResult<TData, TError> {
if (process.env.NODE_ENV !== 'production') {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new Error(
'Bad argument type. Starting with v5, only the "Object" form is allowed when calling query related functions. Please use the error stack to find the culprit call. More info here: https://tanstack.com/query/latest/docs/react/guides/migrating-to-v5#supports-a-single-signature-one-object',
)
}
}
const isRestoring = useIsRestoring()
const errorResetBoundary = useQueryErrorResetBoundary()
const client = useQueryClient(queryClient)
const defaultedOptions = client.defaultQueryOptions(options)
const query = client
.getQueryCache()
.get<TQueryFnData, TError, TQueryData, TQueryKey>(
defaultedOptions.queryHash,
)
if (process.env.NODE_ENV !== 'production') {
if (!defaultedOptions.queryFn) {
console.error(
`[${defaultedOptions.queryHash}]: No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn. More info here: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function`,
)
}
}
defaultedOptions._optimisticResults = isRestoring
? 'isRestoring'
: 'optimistic'
ensureSuspenseTimers(defaultedOptions)
ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query)
useClearResetErrorBoundary(errorResetBoundary)
const [observer] = useState(
() =>
new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
client,
defaultedOptions,
),
)
const result = observer.getOptimisticResult(defaultedOptions)
const shouldSubscribe = !isRestoring && options.subscribed !== false
useSyncExternalStore(
useCallback(
(onStoreChange) => {
const unsubscribe = shouldSubscribe
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
: noop
observer.updateResult()
return unsubscribe
},
[observer, shouldSubscribe],
),
() => observer.getCurrentResult(),
)
useEffect(() => {
observer.setOptions(defaultedOptions)
}, [defaultedOptions, observer])
if (shouldSuspend(defaultedOptions, result)) {
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
}
if (
getHasError({
result,
errorResetBoundary,
throwOnError: defaultedOptions.throwOnError,
query,
suspense: defaultedOptions.suspense,
})
) {
throw result.error
}
return !defaultedOptions.notifyOnChangeProps
? observer.trackResult(result)
: result
}