Skip to content
Open
186 changes: 186 additions & 0 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5920,6 +5920,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({
Expand Down Expand Up @@ -5964,6 +5965,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,
Expand Down Expand Up @@ -5992,6 +5994,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,
Expand Down Expand Up @@ -6191,6 +6194,7 @@ describe('useQuery', () => {
await sleep(5)
return { numbers: { current: { id } } }
}

function Test() {
const [id, setId] = React.useState(1)

Expand Down Expand Up @@ -6256,6 +6260,7 @@ describe('useQuery', () => {
await sleep(5)
return { numbers: { current: { id } } }
}

function Test() {
const [id, setId] = React.useState(1)

Expand Down Expand Up @@ -6761,10 +6766,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, <Example />)

expect(consoleErrorMock).toHaveBeenCalledTimes(1)
Expand All @@ -6774,4 +6781,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 (
<div>
<div data-testid="status">{status}</div>
{error && <div data-testid="error">{error.message}</div>}
</div>
)
}

const { unmount, getByTestId } = renderWithClient(
queryClient,
<Component />,
)

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, <Component />)

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 (
<div>
<div data-testid="status">{status}</div>
{error && <div data-testid="error">{error.message}</div>}
</div>
)
}

const { unmount, getByTestId } = renderWithClient(
queryClient,
<ErrorBoundary
fallbackRender={({ error }) => (
<div>
<div data-testid="status">error</div>
<div data-testid="error">{error?.message}</div>
</div>
)}
>
<Component />
</ErrorBoundary>,
)

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,
<ErrorBoundary
fallbackRender={({ error }) => (
<div>
<div data-testid="status">error</div>
<div data-testid="error">{error?.message}</div>
</div>
)}
>
<Component />
</ErrorBoundary>,
)

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 (
<div>
<div data-testid="status">{status}</div>
{error && <div data-testid="error">{error.message}</div>}
</div>
)
}

const { unmount, getByTestId } = renderWithClient(
queryClient,
<Component />,
)

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, <Component />)

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)
})
})
18 changes: 13 additions & 5 deletions packages/react-query/src/errorBoundaryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,24 @@ export const ensurePreventErrorBoundaryRetry = <
TQueryKey
>,
errorResetBoundary: QueryErrorResetBoundaryValue,
query?: Query<TQueryFnData, TError, TQueryData, TQueryKey>,
) => {
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
}
}
}

Expand Down
11 changes: 9 additions & 2 deletions packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
7 changes: 4 additions & 3 deletions packages/react-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down