diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 893abb03b6..87faf34399 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,183 @@ 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) + }) + + 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) + }) }) diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 28c11c0b10..ccd3993f2e 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -25,16 +25,24 @@ export const ensurePreventErrorBoundaryRetry = < TQueryKey >, errorResetBoundary: QueryErrorResetBoundaryValue, + query?: Query, ) => { - if ( - options.suspense || - options.throwOnError || - 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]) + ) { + options.retryOnMount = false + } + } else { + options.retryOnMount = false + } } } 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)