Skip to content

Commit e4138ec

Browse files
fix(react-query): non continuous suspense with useSuspenseQueries (#6298) (#6303)
Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent fe49dd3 commit e4138ec

File tree

2 files changed

+151
-3
lines changed

2 files changed

+151
-3
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import {
2+
afterAll,
3+
afterEach,
4+
beforeAll,
5+
describe,
6+
expect,
7+
it,
8+
vi,
9+
} from 'vitest'
10+
import { act, render } from '@testing-library/react'
11+
import React from 'react'
12+
import { useSuspenseQueries } from '..'
13+
import { createQueryClient, sleep } from './utils'
14+
import type { UseSuspenseQueryOptions } from '..'
15+
16+
type NumberQueryOptions = UseSuspenseQueryOptions<number>
17+
18+
const QUERY_DURATION = 1000
19+
20+
const createQuery: (id: number) => NumberQueryOptions = (id) => ({
21+
queryKey: [id],
22+
queryFn: async () => {
23+
await sleep(QUERY_DURATION)
24+
return id
25+
},
26+
})
27+
const resolveQueries = () => vi.advanceTimersByTimeAsync(QUERY_DURATION)
28+
29+
const queryClient = createQueryClient()
30+
31+
describe('useSuspenseQueries', () => {
32+
const onSuspend = vi.fn()
33+
const onQueriesResolution = vi.fn()
34+
35+
beforeAll(() => {
36+
vi.useFakeTimers()
37+
})
38+
39+
afterAll(() => {
40+
vi.useRealTimers()
41+
})
42+
43+
afterEach(() => {
44+
queryClient.clear()
45+
onSuspend.mockClear()
46+
onQueriesResolution.mockClear()
47+
})
48+
49+
function SuspenseFallback() {
50+
React.useEffect(() => {
51+
onSuspend()
52+
}, [])
53+
54+
return null
55+
}
56+
57+
const withSuspenseWrapper = <T extends object>(Component: React.FC<T>) => {
58+
function SuspendedComponent(props: T) {
59+
return (
60+
<React.Suspense fallback={<SuspenseFallback />}>
61+
<Component {...props} />
62+
</React.Suspense>
63+
)
64+
}
65+
66+
return SuspendedComponent
67+
}
68+
69+
function QueriesContainer({
70+
queries,
71+
}: {
72+
queries: Array<NumberQueryOptions>
73+
}) {
74+
const queriesResults = useSuspenseQueries(
75+
{ queries, combine: (results) => results.map((r) => r.data) },
76+
queryClient,
77+
)
78+
79+
React.useEffect(() => {
80+
onQueriesResolution(queriesResults)
81+
}, [queriesResults])
82+
83+
return null
84+
}
85+
86+
const TestComponent = withSuspenseWrapper(QueriesContainer)
87+
88+
it('should suspend on mount', () => {
89+
render(<TestComponent queries={[1, 2].map(createQuery)} />)
90+
91+
expect(onSuspend).toHaveBeenCalledOnce()
92+
})
93+
94+
it('should resolve queries', async () => {
95+
render(<TestComponent queries={[1, 2].map(createQuery)} />)
96+
97+
await act(resolveQueries)
98+
99+
expect(onQueriesResolution).toHaveBeenCalledTimes(1)
100+
expect(onQueriesResolution).toHaveBeenLastCalledWith([1, 2])
101+
})
102+
103+
it('should not suspend on mount if query has been already fetched', () => {
104+
const query = createQuery(1)
105+
106+
queryClient.setQueryData(query.queryKey, query.queryFn)
107+
108+
render(<TestComponent queries={[query]} />)
109+
110+
expect(onSuspend).not.toHaveBeenCalled()
111+
})
112+
113+
it('should not break suspense when queries change without resolving', async () => {
114+
const initQueries = [1, 2].map(createQuery)
115+
const nextQueries = [3, 4, 5, 6].map(createQuery)
116+
117+
const { rerender } = render(<TestComponent queries={initQueries} />)
118+
119+
rerender(<TestComponent queries={nextQueries} />)
120+
121+
await act(resolveQueries)
122+
123+
expect(onSuspend).toHaveBeenCalledTimes(1)
124+
expect(onQueriesResolution).toHaveBeenCalledTimes(1)
125+
expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6])
126+
})
127+
128+
it('should suspend only once per queries change', async () => {
129+
const initQueries = [1, 2].map(createQuery)
130+
const nextQueries = [3, 4, 5, 6].map(createQuery)
131+
132+
const { rerender } = render(<TestComponent queries={initQueries} />)
133+
134+
await act(resolveQueries)
135+
136+
rerender(<TestComponent queries={nextQueries} />)
137+
138+
await act(resolveQueries)
139+
140+
expect(onSuspend).toHaveBeenCalledTimes(2)
141+
expect(onQueriesResolution).toHaveBeenCalledTimes(2)
142+
expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6])
143+
})
144+
})

packages/react-query/src/useQueries.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
'use client'
22
import * as React from 'react'
33

4-
import { QueriesObserver, notifyManager } from '@tanstack/query-core'
4+
import {
5+
QueriesObserver,
6+
QueryObserver,
7+
notifyManager,
8+
} from '@tanstack/query-core'
59
import { useQueryClient } from './QueryClientProvider'
610
import { useIsRestoring } from './isRestoring'
711
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
@@ -262,9 +266,9 @@ export function useQueries<
262266
const suspensePromises = shouldAtLeastOneSuspend
263267
? optimisticResult.flatMap((result, index) => {
264268
const opts = defaultedQueries[index]
265-
const queryObserver = observer.getObservers()[index]
266269

267-
if (opts && queryObserver) {
270+
if (opts) {
271+
const queryObserver = new QueryObserver(client, opts)
268272
if (shouldSuspend(opts, result, isRestoring)) {
269273
return fetchOptimistic(opts, queryObserver, errorResetBoundary)
270274
} else if (willFetch(result, isRestoring)) {

0 commit comments

Comments
 (0)