Skip to content

Commit c96178b

Browse files
committed
fix(query-core): handle combine throwing in QueriesObserver#notify
When useSuspenseQueries is used with combine and a query transitions to pending/error state (e.g. after resetQueries), the combine function throws because data is undefined despite types narrowing it as defined. The #notify method calls combine as an optimization to check if the combined result changed. If combine throws during this check, we now catch the error and still notify listeners so the framework can re-suspend or show an error boundary. Fixes #10129
1 parent 67cf8b6 commit c96178b

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,43 @@ describe('queriesObserver', () => {
546546

547547
trackPropSpy.mockRestore()
548548
})
549+
550+
test('should still notify listeners when combine throws after query reset', async () => {
551+
const key1 = queryKey()
552+
const queryFn1 = vi.fn().mockReturnValue({ name: 'test' })
553+
554+
const combine = vi.fn(
555+
(results: Array<QueryObserverResult>) => {
556+
// This simulates a combine function that assumes data is always defined
557+
// (like useSuspenseQueries types suggest)
558+
return results.map((r) => (r.data as { name: string }).name)
559+
},
560+
)
561+
562+
const observer = new QueriesObserver<Array<string>>(
563+
queryClient,
564+
[{ queryKey: key1, queryFn: queryFn1 }],
565+
{ combine },
566+
)
567+
568+
const results: Array<Array<QueryObserverResult>> = []
569+
const unsubscribe = observer.subscribe((result) => {
570+
results.push(result)
571+
})
572+
573+
// Wait for queries to resolve
574+
await vi.advanceTimersByTimeAsync(0)
575+
576+
// Reset the query - this transitions it to pending state
577+
// which should cause combine to throw since data is undefined
578+
queryClient.resetQueries({ queryKey: key1 })
579+
580+
// The listener should still have been notified despite combine throwing
581+
const lastResult = results[results.length - 1]
582+
expect(lastResult).toBeDefined()
583+
expect(lastResult![0]!.status).toBe('pending')
584+
expect(lastResult![0]!.data).toBeUndefined()
585+
586+
unsubscribe()
587+
})
549588
})

packages/query-core/src/queriesObserver.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,23 @@ export class QueriesObserver<
296296
if (this.hasListeners()) {
297297
const previousResult = this.#combinedResult
298298
const newTracked = this.#trackResult(this.#result, this.#observerMatches)
299-
const newResult = this.#combineResult(newTracked, this.#options?.combine)
300299

301-
if (previousResult !== newResult) {
300+
let shouldNotify: boolean
301+
try {
302+
const newResult = this.#combineResult(
303+
newTracked,
304+
this.#options?.combine,
305+
)
306+
shouldNotify = previousResult !== newResult
307+
} catch {
308+
// If combine throws (e.g. when used with useSuspenseQueries and
309+
// a query transitions to pending/error state after a reset), we
310+
// still need to notify so the framework can re-suspend or show
311+
// an error boundary.
312+
shouldNotify = true
313+
}
314+
315+
if (shouldNotify) {
302316
notifyManager.batch(() => {
303317
this.listeners.forEach((listener) => {
304318
listener(this.#result)

0 commit comments

Comments
 (0)