From 76bf8efef32f7d47909a99aae0130e560dabafd7 Mon Sep 17 00:00:00 2001 From: contactjawad Date: Wed, 19 Aug 2026 11:09:45 +0500 Subject: [PATCH 1/2] fix(query-core): notify sibling observers when one unsubscribes during dispatch Query.#dispatch iterated the live observers array while onQueryUpdate() could synchronously splice it (via removeObserver). When one observer unsubscribed a same-query sibling during notification, forEach skipped the next observer, leaving a still-subscribed observer with a stale `pending` result after the query had resolved. Iterate over a snapshot of the observers instead. --- .../fix-query-dispatch-observer-skip.md | 5 +++ .../src/__tests__/queryObserver.test.tsx | 31 +++++++++++++++++++ packages/query-core/src/query.ts | 4 ++- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-query-dispatch-observer-skip.md diff --git a/.changeset/fix-query-dispatch-observer-skip.md b/.changeset/fix-query-dispatch-observer-skip.md new file mode 100644 index 0000000000..99665853b1 --- /dev/null +++ b/.changeset/fix-query-dispatch-observer-skip.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Fix an observer being skipped during query notification when another observer on the same query unsubscribes mid-dispatch. `Query.#dispatch` iterated the live `observers` array while `onQueryUpdate()` could splice it, so a still-subscribed sibling could be left with a stale `pending` result after the query resolved. The notification now iterates over a snapshot of the observers. diff --git a/packages/query-core/src/__tests__/queryObserver.test.tsx b/packages/query-core/src/__tests__/queryObserver.test.tsx index 08fac746a4..a115b879c5 100644 --- a/packages/query-core/src/__tests__/queryObserver.test.tsx +++ b/packages/query-core/src/__tests__/queryObserver.test.tsx @@ -31,6 +31,37 @@ describe('queryObserver', () => { vi.useRealTimers() }) + it('should notify a sibling observer when another observer on the same query unsubscribes during dispatch', async () => { + const key = queryKey() + const queryFn = vi.fn().mockImplementation(() => sleep(10).then(() => 'data')) + + const observerA = new QueryObserver(queryClient, { + queryKey: key, + queryFn, + staleTime: Infinity, + }) + const observerB = new QueryObserver(queryClient, { + queryKey: key, + queryFn, + staleTime: Infinity, + }) + + let unsubscribeA = () => {} + unsubscribeA = observerA.subscribe((result) => { + if (result.status === 'success') { + unsubscribeA() + } + }) + const unsubscribeB = observerB.subscribe(() => undefined) + + await vi.advanceTimersByTimeAsync(15) + + expect(observerB.getCurrentResult().status).toBe('success') + expect(observerB.getCurrentResult().data).toBe('data') + + unsubscribeB() + }) + it('should trigger a fetch when subscribed', () => { const key = queryKey() const queryFn = vi diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index 53811c8ebb..37617e538d 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -704,7 +704,9 @@ export class Query< this.state = reducer(this.state) notifyManager.batch(() => { - this.observers.forEach((observer) => { + // Iterate over a snapshot so that an observer unsubscribing during + // notification cannot cause a sibling observer to be skipped. + ;[...this.observers].forEach((observer) => { observer.onQueryUpdate() }) From 4f220aab1ef39d3b05f33d85d048dd135c294d7d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:20:19 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- packages/query-core/src/__tests__/queryObserver.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryObserver.test.tsx b/packages/query-core/src/__tests__/queryObserver.test.tsx index a115b879c5..8654a28055 100644 --- a/packages/query-core/src/__tests__/queryObserver.test.tsx +++ b/packages/query-core/src/__tests__/queryObserver.test.tsx @@ -33,7 +33,9 @@ describe('queryObserver', () => { it('should notify a sibling observer when another observer on the same query unsubscribes during dispatch', async () => { const key = queryKey() - const queryFn = vi.fn().mockImplementation(() => sleep(10).then(() => 'data')) + const queryFn = vi + .fn() + .mockImplementation(() => sleep(10).then(() => 'data')) const observerA = new QueryObserver(queryClient, { queryKey: key,