From bf3034c44f560dc36e82133794f961b3b81588a4 Mon Sep 17 00:00:00 2001 From: sangwook Date: Mon, 30 Jun 2025 23:13:14 +0900 Subject: [PATCH 1/5] test(react-query): add tests should retry on mount when throwOnError returns false --- .../src/__tests__/useQuery.test.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 893abb03b6..1b0e13d179 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -6027,6 +6027,7 @@ describe('useQuery', () => { it('should be able to toggle subscribed', async () => { const key = queryKey() const queryFn = vi.fn(() => Promise.resolve('data')) + function Page() { const [subscribed, setSubscribed] = React.useState(true) const { data } = useQuery({ @@ -6069,6 +6070,7 @@ describe('useQuery', () => { it('should not be attached to the query when subscribed is false', async () => { const key = queryKey() const queryFn = vi.fn(() => Promise.resolve('data')) + function Page() { const { data } = useQuery({ queryKey: key, @@ -6095,6 +6097,7 @@ describe('useQuery', () => { it('should not re-render when data is added to the cache when subscribed is false', async () => { const key = queryKey() let renders = 0 + function Page() { const { data } = useQuery({ queryKey: key, @@ -6297,6 +6300,7 @@ describe('useQuery', () => { await sleep(5) return { numbers: { current: { id } } } } + function Test() { const [id, setId] = React.useState(1) @@ -6357,6 +6361,7 @@ describe('useQuery', () => { await sleep(5) return { numbers: { current: { id } } } } + function Test() { const [id, setId] = React.useState(1) @@ -6854,10 +6859,12 @@ describe('useQuery', () => { it('should console.error when there is no queryFn', () => { const consoleErrorMock = vi.spyOn(console, 'error') const key = queryKey() + function Example() { useQuery({ queryKey: key }) return <> } + renderWithClient(queryClient, ) expect(consoleErrorMock).toHaveBeenCalledTimes(1) @@ -6867,4 +6874,56 @@ describe('useQuery', () => { consoleErrorMock.mockRestore() }) + + 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 { unmount, getByTestId } = renderWithClient( + queryClient, + , + ) + + await vi.waitFor(() => + expect(getByTestId('status')).toHaveTextContent('error'), + ) + expect(getByTestId('error')).toHaveTextContent('Simulated 500 error') + expect(fetchCount).toBe(1) + + unmount() + + const initialFetchCount = fetchCount + + renderWithClient(queryClient, ) + + await vi.waitFor(() => + expect(getByTestId('status')).toHaveTextContent('error'), + ) + + expect(fetchCount).toBe(initialFetchCount + 1) + expect(queryFn).toHaveBeenCalledTimes(2) + }) }) From 9b4d31b27bcdd6ebc1f2d6f44810be96f805b1d7 Mon Sep 17 00:00:00 2001 From: sangwook Date: Mon, 30 Jun 2025 23:52:02 +0900 Subject: [PATCH 2/5] fix(react-query): refine throwOnError retry logic for error boundary cases When throwOnError is a function, allow retryOnMount to proceed even when error boundary hasn't reset, while maintaining the prevention behavior for boolean throwOnError values. --- packages/react-query/src/errorBoundaryUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 28c11c0b10..52f511fb1b 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -28,13 +28,16 @@ export const ensurePreventErrorBoundaryRetry = < ) => { if ( options.suspense || - options.throwOnError || options.experimental_prefetchInRender ) { // Prevent retrying failed query if the error boundary has not been reset yet if (!errorResetBoundary.isReset()) { options.retryOnMount = false } + } else if (options.throwOnError) { + if (!errorResetBoundary.isReset() && typeof options.throwOnError !== 'function') { + options.retryOnMount = false + } } } From b79395d6dba650621855cd6b5ae8be69fe0a93e4 Mon Sep 17 00:00:00 2001 From: sangwook Date: Tue, 1 Jul 2025 00:19:42 +0900 Subject: [PATCH 3/5] fix(react-query): enhance throwOnError logic in error boundaries Refine behavior to handle function-type throwOnError, allowing retries when appropriate. Ensure boolean throwOnError values still prevent retries when the error boundary isn't reset. --- packages/react-query/src/errorBoundaryUtils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 52f511fb1b..7e7b44afcd 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -25,6 +25,7 @@ export const ensurePreventErrorBoundaryRetry = < TQueryKey >, errorResetBoundary: QueryErrorResetBoundaryValue, + query?: Query, ) => { if ( options.suspense || @@ -34,8 +35,12 @@ export const ensurePreventErrorBoundaryRetry = < if (!errorResetBoundary.isReset()) { options.retryOnMount = false } - } else if (options.throwOnError) { - if (!errorResetBoundary.isReset() && typeof options.throwOnError !== 'function') { + } else if (options.throwOnError && !errorResetBoundary.isReset()) { + if (typeof options.throwOnError === 'function') { + if (query?.state.error && shouldThrowError(options.throwOnError, [query.state.error, query])) { + options.retryOnMount = false + } + } else { options.retryOnMount = false } } From bbc97236ba5e78057dff43cb6fe4763c41620ef7 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sun, 6 Jul 2025 09:59:21 +0900 Subject: [PATCH 4/5] test: Add tests for `throwOnError` behavior in `useQuery` This commit introduces tests to verify the behavior of the `throwOnError` callback in scenarios where `retryOnMount` is enabled. It ensures proper handling of retries based on the error state and the `throwOnError` function's return value. These tests improve the reliability and coverage of error handling logic in `useQuery`. --- .../src/__tests__/useQuery.test.tsx | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 1b0e13d179..e137a30ae9 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -6926,4 +6926,130 @@ describe('useQuery', () => { 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 { unmount, getByTestId } = renderWithClient( + queryClient, + ( +
+
error
+
{error?.message}
+
+ )} + > + +
, + ) + + await vi.waitFor(() => + expect(getByTestId('status')).toHaveTextContent('error'), + ) + expect(getByTestId('error')).toHaveTextContent('Simulated 500 error') + expect(fetchCount).toBe(1) + + unmount() + + const initialFetchCount = fetchCount + + renderWithClient(queryClient, + ( +
+
error
+
{error?.message}
+
+ )} + > + +
+ ) + + await vi.waitFor(() => + expect(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 { unmount, getByTestId } = renderWithClient( + queryClient, + , + ) + + await vi.waitFor(() => + expect(getByTestId('status')).toHaveTextContent('error'), + ) + expect(getByTestId('error')).toHaveTextContent('Simulated 500 error') + expect(fetchCount).toBe(1) + + unmount() + + const initialFetchCount = fetchCount + + renderWithClient(queryClient, ) + + await vi.waitFor(() => + expect(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) + }) }) From 9c8516d666ecd8db07febbcac453bdf408cd2d89 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sun, 6 Jul 2025 10:28:30 +0900 Subject: [PATCH 5/5] fix(react-query): improve throwOnError logic with query state validation - Pass query object to ensurePreventErrorBoundaryRetry for accurate state checking - Preserve query deduplication behavior while fixing throwOnError function handling - Fixes issue where throwOnError function couldn't access query error state --- packages/react-query/src/__tests__/useQuery.test.tsx | 5 +++-- packages/react-query/src/errorBoundaryUtils.ts | 10 +++++----- packages/react-query/src/useBaseQuery.ts | 11 +++++++++-- packages/react-query/src/useQueries.ts | 7 ++++--- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index e137a30ae9..87faf34399 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -6978,7 +6978,8 @@ describe('useQuery', () => { const initialFetchCount = fetchCount - renderWithClient(queryClient, + renderWithClient( + queryClient, (
@@ -6988,7 +6989,7 @@ describe('useQuery', () => { )} > - + , ) await vi.waitFor(() => diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 7e7b44afcd..ccd3993f2e 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -27,17 +27,17 @@ export const ensurePreventErrorBoundaryRetry = < errorResetBoundary: QueryErrorResetBoundaryValue, query?: Query, ) => { - if ( - options.suspense || - options.experimental_prefetchInRender - ) { + if (options.suspense || options.experimental_prefetchInRender) { // Prevent retrying failed query if the error boundary has not been reset yet if (!errorResetBoundary.isReset()) { options.retryOnMount = false } } else if (options.throwOnError && !errorResetBoundary.isReset()) { if (typeof options.throwOnError === 'function') { - if (query?.state.error && shouldThrowError(options.throwOnError, [query.state.error, query])) { + if ( + query?.state.error && + shouldThrowError(options.throwOnError, [query.state.error, query]) + ) { options.retryOnMount = false } } else { diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index 06690b544f..4158c5734f 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -53,6 +53,14 @@ export function useBaseQuery< const errorResetBoundary = useQueryErrorResetBoundary() const client = useQueryClient(queryClient) const defaultedOptions = client.defaultQueryOptions(options) + const query = client + .getQueryCache() + .get< + TQueryFnData, + TError, + TQueryData, + TQueryKey + >(defaultedOptions.queryHash) ;(client.getDefaultOptions().queries as any)?._experimental_beforeQuery?.( defaultedOptions, @@ -72,8 +80,7 @@ export function useBaseQuery< : 'optimistic' ensureSuspenseTimers(defaultedOptions) - ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary) - + ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query) useClearResetErrorBoundary(errorResetBoundary) // this needs to be invoked before creating the Observer because that can create a cache entry diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index a736f5cd1d..6eabef4060 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -242,9 +242,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)