Skip to content

Commit c744f99

Browse files
authoredAug 8, 2024··
fix(react-query): ensure we have a gcTime of at least 1 second when using suspense (#7860)
Because React continues to render components that have already thrown to error boundaries, a small gcTime would lead to infinite fetches because if the cache entry is already removed, the next render will treat the extra render as a new suspending query.
1 parent 1fc6124 commit c744f99

File tree

5 files changed

+59
-23
lines changed

5 files changed

+59
-23
lines changed
 

‎packages/react-query/src/__tests__/suspense.test.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -1192,4 +1192,48 @@ describe('useSuspenseQueries', () => {
11921192

11931193
await waitFor(() => rendered.getByText('data1'))
11941194
})
1195+
1196+
it('should show error boundary even with gcTime:0 (#7853)', async () => {
1197+
const consoleMock = vi
1198+
.spyOn(console, 'error')
1199+
.mockImplementation(() => undefined)
1200+
const key = queryKey()
1201+
let count = 0
1202+
1203+
function Page() {
1204+
useSuspenseQuery({
1205+
queryKey: key,
1206+
queryFn: async () => {
1207+
count++
1208+
console.log('queryFn')
1209+
throw new Error('Query failed')
1210+
},
1211+
gcTime: 0,
1212+
retry: false,
1213+
})
1214+
1215+
return null
1216+
}
1217+
1218+
function App() {
1219+
return (
1220+
<React.Suspense fallback="loading">
1221+
<ErrorBoundary
1222+
fallbackRender={() => {
1223+
console.log('fallback renders')
1224+
return <div>There was an error!</div>
1225+
}}
1226+
>
1227+
<Page />
1228+
</ErrorBoundary>
1229+
</React.Suspense>
1230+
)
1231+
}
1232+
1233+
const rendered = renderWithClient(queryClient, <App />)
1234+
1235+
await waitFor(() => rendered.getByText('There was an error!'))
1236+
expect(count).toBe(1)
1237+
consoleMock.mockRestore()
1238+
})
11951239
})

‎packages/react-query/src/__tests__/useQuery.test.tsx

+3-18
Original file line numberDiff line numberDiff line change
@@ -4198,17 +4198,15 @@ describe('useQuery', () => {
41984198

41994199
it('should not interval fetch with a refetchInterval of 0', async () => {
42004200
const key = queryKey()
4201-
const states: Array<UseQueryResult<number>> = []
4201+
const queryFn = vi.fn(() => 1)
42024202

42034203
function Page() {
42044204
const queryInfo = useQuery({
42054205
queryKey: key,
4206-
queryFn: () => 1,
4206+
queryFn,
42074207
refetchInterval: 0,
42084208
})
42094209

4210-
states.push(queryInfo)
4211-
42124210
return <div>count: {queryInfo.data}</div>
42134211
}
42144212

@@ -4218,20 +4216,7 @@ describe('useQuery', () => {
42184216

42194217
await sleep(10) // extra sleep to make sure we're not re-fetching
42204218

4221-
expect(states.length).toEqual(2)
4222-
4223-
expect(states).toMatchObject([
4224-
{
4225-
status: 'pending',
4226-
isFetching: true,
4227-
data: undefined,
4228-
},
4229-
{
4230-
status: 'success',
4231-
isFetching: false,
4232-
data: 1,
4233-
},
4234-
])
4219+
expect(queryFn).toHaveBeenCalledTimes(1)
42354220
})
42364221

42374222
it('should accept an empty string as query key', async () => {

‎packages/react-query/src/suspense.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const defaultThrowOnError = <
1818
query: Query<TQueryFnData, TError, TData, TQueryKey>,
1919
) => query.state.data === undefined
2020

21-
export const ensureStaleTime = (
21+
export const ensureSuspenseTimers = (
2222
defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>,
2323
) => {
2424
if (defaultedOptions.suspense) {
@@ -27,6 +27,9 @@ export const ensureStaleTime = (
2727
if (typeof defaultedOptions.staleTime !== 'number') {
2828
defaultedOptions.staleTime = 1000
2929
}
30+
if (typeof defaultedOptions.gcTime === 'number') {
31+
defaultedOptions.gcTime = Math.max(defaultedOptions.gcTime, 1000)
32+
}
3033
}
3134
}
3235

‎packages/react-query/src/useBaseQuery.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
getHasError,
1111
useClearResetErrorBoundary,
1212
} from './errorBoundaryUtils'
13-
import { ensureStaleTime, fetchOptimistic, shouldSuspend } from './suspense'
13+
import {
14+
ensureSuspenseTimers,
15+
fetchOptimistic,
16+
shouldSuspend,
17+
} from './suspense'
1418
import type { UseBaseQueryOptions } from './types'
1519
import type {
1620
QueryClient,
@@ -58,7 +62,7 @@ export function useBaseQuery<
5862
? 'isRestoring'
5963
: 'optimistic'
6064

61-
ensureStaleTime(defaultedOptions)
65+
ensureSuspenseTimers(defaultedOptions)
6266
ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary)
6367

6468
useClearResetErrorBoundary(errorResetBoundary)

‎packages/react-query/src/useQueries.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
useClearResetErrorBoundary,
1616
} from './errorBoundaryUtils'
1717
import {
18-
ensureStaleTime,
18+
ensureSuspenseTimers,
1919
fetchOptimistic,
2020
shouldSuspend,
2121
willFetch,
@@ -255,7 +255,7 @@ export function useQueries<
255255
)
256256

257257
defaultedQueries.forEach((query) => {
258-
ensureStaleTime(query)
258+
ensureSuspenseTimers(query)
259259
ensurePreventErrorBoundaryRetry(query, errorResetBoundary)
260260
})
261261

0 commit comments

Comments
 (0)
Please sign in to comment.