Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-query-dispatch-observer-skip.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions packages/query-core/src/__tests__/queryObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down
Loading