Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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.
33 changes: 33 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,39 @@ 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