diff --git a/.changeset/preact-retry-on-mount-throw-on-error.md b/.changeset/preact-retry-on-mount-throw-on-error.md
new file mode 100644
index 00000000000..21a7fd14a04
--- /dev/null
+++ b/.changeset/preact-retry-on-mount-throw-on-error.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/preact-query': patch
+---
+
+Evaluate a function-form `throwOnError` against the actual query error before disabling `retryOnMount` (port of #9338), so errored queries whose `throwOnError` returns `false` are retried on mount again.
diff --git a/packages/preact-query/src/__tests__/useQuery.test.tsx b/packages/preact-query/src/__tests__/useQuery.test.tsx
index 3a36725ff09..db1971ec4aa 100644
--- a/packages/preact-query/src/__tests__/useQuery.test.tsx
+++ b/packages/preact-query/src/__tests__/useQuery.test.tsx
@@ -6740,4 +6740,171 @@ describe('useQuery', () => {
expect(rendered.getByTestId('data')).toHaveTextContent('undefined')
expect(queryFn).toHaveBeenCalledTimes(0)
})
+ it('should retry on mount when throwOnError returns false', async () => {
+ const key = queryKey()
+ let fetchCount = 0
+ const queryFn = vi.fn().mockImplementation(() => {
+ fetchCount++
+ console.log(`Fetching... (attempt ${fetchCount})`)
+ return Promise.reject(new Error('Simulated 500 error'))
+ })
+
+ function Component() {
+ const { status, error } = useQuery({
+ queryKey: key,
+ queryFn,
+ throwOnError: () => false,
+ retryOnMount: () => true,
+ staleTime: Infinity,
+ retry: false,
+ })
+
+ return (
+
+
{status}
+ {error &&
{error.message}
}
+
+ )
+ }
+
+ const rendered1 = renderWithClient(queryClient, )
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered1.getByTestId('status')).toHaveTextContent('error')
+ expect(rendered1.getByTestId('error')).toHaveTextContent(
+ 'Simulated 500 error',
+ )
+ expect(fetchCount).toBe(1)
+ rendered1.unmount()
+
+ const initialFetchCount = fetchCount
+
+ const rendered2 = renderWithClient(queryClient, )
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered2.getByTestId('status')).toHaveTextContent('error')
+
+ expect(fetchCount).toBe(initialFetchCount + 1)
+ expect(queryFn).toHaveBeenCalledTimes(2)
+ })
+
+ it('should not retry on mount when throwOnError function returns true', async () => {
+ const key = queryKey()
+ let fetchCount = 0
+ const queryFn = vi.fn().mockImplementation(() => {
+ fetchCount++
+ console.log(`Fetching... (attempt ${fetchCount})`)
+ return Promise.reject(new Error('Simulated 500 error'))
+ })
+
+ function Component() {
+ const { status, error } = useQuery({
+ queryKey: key,
+ queryFn,
+ throwOnError: () => true,
+ retryOnMount: () => true,
+ staleTime: Infinity,
+ retry: false,
+ })
+
+ return (
+
+
{status}
+ {error &&
{error.message}
}
+
+ )
+ }
+
+ const rendered1 = renderWithClient(
+ queryClient,
+ (
+
+
error
+
{error?.message}
+
+ )}
+ >
+
+ ,
+ )
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered1.getByTestId('status')).toHaveTextContent('error')
+ expect(rendered1.getByTestId('error')).toHaveTextContent(
+ 'Simulated 500 error',
+ )
+ expect(fetchCount).toBe(1)
+
+ rendered1.unmount()
+
+ const initialFetchCount = fetchCount
+
+ const rendered2 = renderWithClient(
+ queryClient,
+ (
+
+
error
+
{error?.message}
+
+ )}
+ >
+
+ ,
+ )
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered2.getByTestId('status')).toHaveTextContent('error')
+
+ // Should not retry because throwOnError returns true
+ expect(fetchCount).toBe(initialFetchCount)
+ expect(queryFn).toHaveBeenCalledTimes(1)
+ })
+
+ it('should handle throwOnError function based on actual error state', async () => {
+ const key = queryKey()
+ let fetchCount = 0
+ const queryFn = vi.fn().mockImplementation(() => {
+ fetchCount++
+ console.log(`Fetching... (attempt ${fetchCount})`)
+ return Promise.reject(new Error('Simulated 500 error'))
+ })
+
+ function Component() {
+ const { status, error } = useQuery({
+ queryKey: key,
+ queryFn,
+ throwOnError: (error) => error.message.includes('404'),
+ retryOnMount: true,
+ staleTime: Infinity,
+ retry: false,
+ })
+
+ return (
+
+
{status}
+ {error &&
{error.message}
}
+
+ )
+ }
+
+ const rendered1 = renderWithClient(queryClient, )
+
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered1.getByTestId('status')).toHaveTextContent('error')
+ expect(rendered1.getByTestId('error')).toHaveTextContent(
+ 'Simulated 500 error',
+ )
+ expect(fetchCount).toBe(1)
+
+ rendered1.unmount()
+
+ const initialFetchCount = fetchCount
+
+ const rendered2 = renderWithClient(queryClient, )
+
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered2.getByTestId('status')).toHaveTextContent('error')
+
+ // Should retry because throwOnError returns false (500 error doesn't include '404')
+ expect(fetchCount).toBe(initialFetchCount + 1)
+ expect(queryFn).toHaveBeenCalledTimes(2)
+ })
})
diff --git a/packages/preact-query/src/errorBoundaryUtils.ts b/packages/preact-query/src/errorBoundaryUtils.ts
index 0b3d561fea6..86fbccd29f1 100644
--- a/packages/preact-query/src/errorBoundaryUtils.ts
+++ b/packages/preact-query/src/errorBoundaryUtils.ts
@@ -25,11 +25,17 @@ export const ensurePreventErrorBoundaryRetry = <
TQueryKey
>,
errorResetBoundary: QueryErrorResetBoundaryValue,
+ query: Query | undefined,
) => {
+ const throwOnError =
+ query?.state.error && typeof options.throwOnError === 'function'
+ ? shouldThrowError(options.throwOnError, [query.state.error, query])
+ : options.throwOnError
+
if (
options.suspense ||
- options.throwOnError ||
- options.experimental_prefetchInRender
+ options.experimental_prefetchInRender ||
+ throwOnError
) {
// Prevent retrying failed query if the error boundary has not been reset yet
if (!errorResetBoundary.isReset()) {
diff --git a/packages/preact-query/src/useBaseQuery.ts b/packages/preact-query/src/useBaseQuery.ts
index 05ce46e7f6e..ec820b7586b 100644
--- a/packages/preact-query/src/useBaseQuery.ts
+++ b/packages/preact-query/src/useBaseQuery.ts
@@ -58,6 +58,15 @@ export function useBaseQuery<
defaultedOptions,
)
+ const query = client
+ .getQueryCache()
+ .get<
+ TQueryFnData,
+ TError,
+ TQueryData,
+ TQueryKey
+ >(defaultedOptions.queryHash)
+
if (process.env.NODE_ENV !== 'production') {
if (!defaultedOptions.queryFn) {
console.error(
@@ -72,7 +81,7 @@ export function useBaseQuery<
: 'optimistic'
ensureSuspenseTimers(defaultedOptions)
- ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary)
+ ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query)
useClearResetErrorBoundary(errorResetBoundary)
@@ -126,14 +135,7 @@ export function useBaseQuery<
result,
errorResetBoundary,
throwOnError: defaultedOptions.throwOnError,
- query: client
- .getQueryCache()
- .get<
- TQueryFnData,
- TError,
- TQueryData,
- TQueryKey
- >(defaultedOptions.queryHash),
+ query,
suspense: defaultedOptions.suspense,
})
) {
@@ -154,7 +156,7 @@ export function useBaseQuery<
? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted
fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
: // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in
- client.getQueryCache().get(defaultedOptions.queryHash)?.promise
+ query?.promise
promise?.catch(noop).finally(() => {
// `.updateResult()` will trigger `.#currentThenable` to finalize
diff --git a/packages/preact-query/src/useQueries.ts b/packages/preact-query/src/useQueries.ts
index 7e30658b843..dac252d0109 100644
--- a/packages/preact-query/src/useQueries.ts
+++ b/packages/preact-query/src/useQueries.ts
@@ -241,9 +241,10 @@ export function useQueries<
[queries, client, isRestoring],
)
- defaultedQueries.forEach((query) => {
- ensureSuspenseTimers(query)
- ensurePreventErrorBoundaryRetry(query, errorResetBoundary)
+ defaultedQueries.forEach((queryOptions) => {
+ ensureSuspenseTimers(queryOptions)
+ const query = client.getQueryCache().get(queryOptions.queryHash)
+ ensurePreventErrorBoundaryRetry(queryOptions, errorResetBoundary, query)
})
useClearResetErrorBoundary(errorResetBoundary)