Skip to content

Commit f85d825

Browse files
Feature/use suspense queries combine (#10576)
* useSuspenseQueries combine * useSuspenseQueries combine * ci: apply automated fixes * fix: stale data * ci: apply automated fixes (attempt 3/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 93b2845 commit f85d825

4 files changed

Lines changed: 167 additions & 3 deletions

File tree

.changeset/wild-rabbits-jump.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@tanstack/react-query': patch
3+
'@tanstack/query-core': patch
4+
---
5+
6+
fix(suspense): skip calling combine when queries would suspend

packages/query-core/src/__tests__/queriesObserver.test.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,82 @@ describe('queriesObserver', () => {
473473
expect(newCombined.count).toBe(2)
474474
})
475475

476+
it('should skip combine notifications while suspense queries have no data', async () => {
477+
const key = queryKey()
478+
const combine = vi.fn((results: Array<QueryObserverResult>) =>
479+
results.map((result) => result.data),
480+
)
481+
const query = {
482+
queryKey: key,
483+
queryFn: () => sleep(10).then(() => 'data'),
484+
staleTime: Infinity,
485+
suspense: true,
486+
}
487+
488+
queryClient.setQueryData(key, 'data')
489+
490+
const observer = new QueriesObserver<Array<unknown>>(queryClient, [query], {
491+
combine,
492+
})
493+
494+
const [rawResult, getCombinedResult] = observer.getOptimisticResult(
495+
[query],
496+
combine,
497+
)
498+
expect(getCombinedResult(rawResult)).toEqual(['data'])
499+
expect(combine).toHaveBeenCalledTimes(1)
500+
501+
const unsubscribe = observer.subscribe(() => undefined)
502+
503+
void queryClient.resetQueries({ queryKey: key })
504+
expect(combine).toHaveBeenCalledTimes(1)
505+
506+
unsubscribe()
507+
})
508+
509+
it('should skip combine notifications after suspense is enabled without structural changes', async () => {
510+
const key = queryKey()
511+
const combine = vi.fn((results: Array<QueryObserverResult>) =>
512+
results.map((result) => result.data),
513+
)
514+
const query = {
515+
queryKey: key,
516+
queryFn: () => sleep(10).then(() => 'data'),
517+
staleTime: Infinity,
518+
suspense: false,
519+
}
520+
521+
queryClient.setQueryData(key, 'data')
522+
523+
const observer = new QueriesObserver<Array<unknown>>(queryClient, [query], {
524+
combine,
525+
})
526+
527+
const [rawResult, getCombinedResult] = observer.getOptimisticResult(
528+
[query],
529+
combine,
530+
)
531+
expect(getCombinedResult(rawResult)).toEqual(['data'])
532+
expect(combine).toHaveBeenCalledTimes(1)
533+
534+
const unsubscribe = observer.subscribe(() => undefined)
535+
536+
observer.setQueries(
537+
[
538+
{
539+
...query,
540+
suspense: true,
541+
},
542+
],
543+
{ combine },
544+
)
545+
546+
void queryClient.resetQueries({ queryKey: key })
547+
expect(combine).toHaveBeenCalledTimes(1)
548+
549+
unsubscribe()
550+
})
551+
476552
it('should handle queries being removed with stable combine reference', () => {
477553
const combine = vi.fn((results: Array<QueryObserverResult>) => ({
478554
count: results.length,

packages/query-core/src/queriesObserver.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,17 @@ export class QueriesObserver<
249249
return input as any
250250
}
251251

252+
#shouldSkipCombine(): boolean {
253+
return (
254+
this.#options?.combine !== undefined &&
255+
this.#observers.some((observer, index) => {
256+
return (
257+
observer.options.suspense && this.#result[index]?.data === undefined
258+
)
259+
})
260+
)
261+
}
262+
252263
#findMatchingObservers(
253264
queries: Array<QueryObserverOptions>,
254265
): Array<QueryObserverMatch> {
@@ -294,11 +305,14 @@ export class QueriesObserver<
294305

295306
#notify(): void {
296307
if (this.hasListeners()) {
297-
const previousResult = this.#combinedResult
298308
const newTracked = this.#trackResult(this.#result, this.#observerMatches)
299-
const newResult = this.#combineResult(newTracked, this.#options?.combine)
309+
const shouldSkipCombine = this.#shouldSkipCombine()
310+
const previousResult = this.#combinedResult
311+
const newResult = shouldSkipCombine
312+
? previousResult
313+
: this.#combineResult(newTracked, this.#options?.combine)
300314

301-
if (previousResult !== newResult) {
315+
if (shouldSkipCombine || previousResult !== newResult) {
302316
notifyManager.batch(() => {
303317
this.listeners.forEach((listener) => {
304318
listener(this.#result)

packages/react-query/src/__tests__/useSuspenseQueries.test.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,74 @@ describe('useSuspenseQueries', () => {
274274
expect(spy).toHaveBeenCalled()
275275
})
276276

277+
it('should not call combine while reset queries are pending again', async () => {
278+
const consoleMock = vi
279+
.spyOn(console, 'error')
280+
.mockImplementation(() => undefined)
281+
const key = queryKey()
282+
let shouldError = false
283+
284+
function Page() {
285+
const data = useSuspenseQueries({
286+
queries: [
287+
{
288+
queryKey: key,
289+
queryFn: () =>
290+
sleep(10).then(() => {
291+
if (shouldError) {
292+
throw new Error('Suspense Error Bingo')
293+
}
294+
295+
return 'data'
296+
}),
297+
retry: false,
298+
},
299+
],
300+
combine: (result) => result.map((query) => query.data.toUpperCase()),
301+
})
302+
303+
return (
304+
<div>
305+
<button
306+
onClick={() => void queryClient.resetQueries({ queryKey: key })}
307+
>
308+
reset
309+
</button>
310+
<div>data: {data.join(',')}</div>
311+
</div>
312+
)
313+
}
314+
315+
const rendered = renderWithClient(
316+
queryClient,
317+
<ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
318+
<React.Suspense fallback="loading">
319+
<Page />
320+
</React.Suspense>
321+
</ErrorBoundary>,
322+
)
323+
324+
expect(rendered.getByText('loading')).toBeInTheDocument()
325+
326+
await act(() => vi.advanceTimersByTimeAsync(10))
327+
expect(rendered.getByText('data: DATA')).toBeInTheDocument()
328+
329+
shouldError = true
330+
331+
expect(() => {
332+
fireEvent.click(rendered.getByText('reset'))
333+
}).not.toThrow()
334+
335+
await act(() => vi.advanceTimersByTimeAsync(10))
336+
expect(rendered.getByText('error boundary')).toBeInTheDocument()
337+
338+
expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual(
339+
new Error('Suspense Error Bingo'),
340+
)
341+
342+
consoleMock.mockRestore()
343+
})
344+
277345
it('should handle duplicate query keys without infinite loops', async () => {
278346
const key = queryKey()
279347
const localDuration = 10

0 commit comments

Comments
 (0)