Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ function shouldFetchOptionally(
return (
options.enabled !== false &&
(query !== prevQuery || prevOptions.enabled === false) &&
(prevQuery.state.status !== 'error' || prevOptions.enabled === false) &&
isStale(query, options)
)
}
Expand Down
70 changes: 70 additions & 0 deletions src/react/tests/suspense.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,74 @@ describe("useQuery's in Suspense mode", () => {
expect(queryFn).toHaveBeenCalledTimes(1)
await waitFor(() => rendered.getByLabelText('fire'))
})

it('should error catched in error boundary without infinite loop', async () => {
const key = queryKey()

const consoleMock = mockConsoleError()

let succeed = true

function Page() {
const [nonce] = React.useState(0)
const queryKeys = `${key}-${succeed}`
const result = useQuery(
queryKeys,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Suspense Error Bingo')
} else {
return nonce
}
},
{
retry: false,
suspense: true,
}
)
return (
<div>
<span>rendered</span> <span>{result.data}</span>
<button
aria-label="fail"
onClick={async () => {
await queryClient.resetQueries()
}}
>
fail
</button>
</div>
)
}

function App() {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
fallbackRender={() => <div>error boundary</div>}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}

const rendered = renderWithClient(queryClient, <App />)

// render suspense fallback (Loading...)
await waitFor(() => rendered.getByText('Loading...'))
// resolve promise -> render Page (rendered)
await waitFor(() => rendered.getByText('rendered'))

// change query key
succeed = false
// reset query -> and throw error
fireEvent.click(rendered.getByLabelText('fail'))
// render error boundary fallback (error boundary)
await waitFor(() => rendered.getByText('error boundary'))
consoleMock.mockRestore()
})
})
61 changes: 61 additions & 0 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3883,4 +3883,65 @@ describe('useQuery', () => {
expect(renders).toBe(2)
expect(hashes).toBe(2)
})

it('should refetch when changed enabled to true in error state', async () => {
const consoleMock = mockConsoleError()

const queryFn = jest.fn()
queryFn.mockImplementation(async () => {
await sleep(10)
return Promise.reject(new Error('Suspense Error Bingo'))
})

function Page({ enabled }: { enabled: boolean }) {
const { error, isLoading } = useQuery(['key'], queryFn, {
enabled,
retry: false,
retryOnMount: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
})

if (isLoading) {
return <div>status: loading</div>
}
if (error instanceof Error) {
return <div>error</div>
}
return <div>rendered</div>
}

function App() {
const [enabled, toggle] = React.useReducer(x => !x, true)

return (
<div>
<Page enabled={enabled} />
<button aria-label="retry" onClick={toggle}>
retry {enabled}
</button>
</div>
)
}

const rendered = renderWithClient(queryClient, <App />)

// initial state check
rendered.getByText('status: loading')

// // render error state component
await waitFor(() => rendered.getByText('error'))
expect(queryFn).toBeCalledTimes(1)

// change to enabled to false
fireEvent.click(rendered.getByLabelText('retry'))
await waitFor(() => rendered.getByText('error'))
expect(queryFn).toBeCalledTimes(1)

// // change to enabled to true
fireEvent.click(rendered.getByLabelText('retry'))
expect(queryFn).toBeCalledTimes(2)

consoleMock.mockRestore()
})
})