Skip to content

Commit d10cca3

Browse files
committed
fix(query-core): ensure query refetches on mount/retry when status is error (#9728)
1 parent aba3260 commit d10cca3

3 files changed

Lines changed: 118 additions & 1 deletion

File tree

.changeset/open-keys-create.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/query-core': patch
3+
---
4+
5+
Fix: Ensure queries refetch on mount or retry when in error state, even if data is not stale.

packages/query-core/src/queryObserver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,11 @@ function shouldFetchOn(
781781
) {
782782
const value = typeof field === 'function' ? field(query) : field
783783

784-
return value === 'always' || (value !== false && isStale(query, options))
784+
return (
785+
value === 'always' ||
786+
(value !== false &&
787+
(isStale(query, options) || query.state.status === 'error'))
788+
)
785789
}
786790
return false
787791
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// @vitest-environment jsdom
2+
import { describe, expect, it, vi } from 'vitest'
3+
import { fireEvent, render } from '@testing-library/react'
4+
import * as React from 'react'
5+
import { ErrorBoundary } from 'react-error-boundary'
6+
import {
7+
QueryClient,
8+
QueryClientProvider,
9+
QueryErrorResetBoundary,
10+
useQuery,
11+
} from '..'
12+
import { queryKey } from '@tanstack/query-test-utils'
13+
14+
describe('issue 9728', () => {
15+
it('should refetch after error when staleTime is Infinity and previous data exists', async () => {
16+
const key = queryKey()
17+
const queryFn = vi.fn()
18+
let count = 0
19+
20+
queryFn.mockImplementation(async () => {
21+
count++
22+
if (count === 2) {
23+
throw new Error('Error ' + count)
24+
}
25+
return 'Success ' + count
26+
})
27+
28+
const queryClient = new QueryClient({
29+
defaultOptions: {
30+
queries: {
31+
retry: false,
32+
staleTime: Infinity,
33+
},
34+
},
35+
})
36+
37+
function Page() {
38+
const [_, forceUpdate] = React.useState(0)
39+
40+
React.useEffect(() => {
41+
forceUpdate(1)
42+
}, [])
43+
44+
const { data, refetch } = useQuery({
45+
queryKey: key,
46+
queryFn,
47+
throwOnError: true,
48+
})
49+
50+
return (
51+
<div>
52+
<div>Data: {data}</div>
53+
<button onClick={() => refetch()}>Refetch</button>
54+
</div>
55+
)
56+
}
57+
58+
function App() {
59+
return (
60+
<QueryErrorResetBoundary>
61+
{({ reset }) => (
62+
<ErrorBoundary
63+
onReset={reset}
64+
fallbackRender={({ resetErrorBoundary }) => (
65+
<div>
66+
<div>Status: error</div>
67+
<button onClick={resetErrorBoundary}>Retry</button>
68+
</div>
69+
)}
70+
>
71+
<React.Suspense fallback={<div>Loading...</div>}>
72+
<Page />
73+
</React.Suspense>
74+
</ErrorBoundary>
75+
)}
76+
</QueryErrorResetBoundary>
77+
)
78+
}
79+
80+
const { getByText, findByText } = render(
81+
<React.StrictMode>
82+
<QueryClientProvider client={queryClient}>
83+
<App />
84+
</QueryClientProvider>
85+
</React.StrictMode>,
86+
)
87+
88+
// 1. First mount -> Success
89+
await findByText('Data: Success 1')
90+
expect(queryFn).toHaveBeenCalledTimes(1)
91+
92+
// 2. Click Refetch -> Triggers fetch -> Fails (Error 2) -> ErrorBoundary
93+
fireEvent.click(getByText('Refetch'))
94+
95+
// Wait for error UI
96+
await findByText('Status: error')
97+
expect(queryFn).toHaveBeenCalledTimes(2)
98+
99+
// 3. Click Retry -> Remounts
100+
// Because staleTime is Infinity and we have Data from (1),
101+
// AND we are in Error state.
102+
fireEvent.click(getByText('Retry'))
103+
104+
// Should call queryFn again (3rd time) and succeed
105+
await findByText('Data: Success 3')
106+
expect(queryFn).toHaveBeenCalledTimes(3)
107+
})
108+
})

0 commit comments

Comments
 (0)