Skip to content
Open
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/afraid-plums-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/angular-query-experimental': patch
---

Register the Angular pending task as soon as a query subscription starts a fetch, so `whenStable()` no longer resolves while the query is still loading
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,9 @@ describe('injectQuery', () => {
TestBed.tick()

const stablePromise = app.whenStable()
// Let the batched observer notification run so the finished fetch is
// reported and the pending task is released
await vi.advanceTimersByTimeAsync(10)
await stablePromise

expect(query.status()).toBe('success')
Expand Down Expand Up @@ -841,7 +844,11 @@ describe('injectQuery', () => {
// Synchronize pending effects
TestBed.tick()

await app.whenStable()
const stablePromise = app.whenStable()
// Let the batched observer notification run so the finished fetch is
// reported and the pending task is released
await vi.advanceTimersByTimeAsync(10)
await stablePromise
expect(query.status()).toBe('success')
expect(query.data()).toBe('sync-data-1')
expect(callCount).toBe(1)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ApplicationRef,
Component,
effect,
provideZonelessChangeDetection,
} from '@angular/core'
import { TestBed } from '@angular/core/testing'
Expand Down Expand Up @@ -155,6 +156,44 @@ describe('PendingTasks Integration', () => {
})
})

// The observer reports a running fetch through a batched notification that
// only lands in a later task. This test uses real timers because it asserts
// on what `whenStable()` does before that notification arrives.
describe('Task registration timing', () => {
it('should let whenStable wait for the fetch a component just started', async () => {
vi.useRealTimers()

const key = queryKey()
const seenInEffect: Array<boolean> = []

@Component({
selector: 'app-timing-query',
template: `{{ query.isSuccess() }}`,
standalone: true,
})
class TimingQueryComponent {
query = injectQuery(() => ({
queryKey: key,
queryFn: () => Promise.resolve('component-data'),
}))

constructor() {
effect(() => {
seenInEffect.push(this.query.isSuccess())
})
}
}

const fixture = TestBed.createComponent(TimingQueryComponent)
fixture.detectChanges()
await fixture.whenStable()

expect(fixture.componentInstance.query.status()).toBe('success')
expect(fixture.componentInstance.query.data()).toBe('component-data')
expect(seenInEffect).toContain(true)
})
})

describe('Race Conditions', () => {
it('should handle query that completes during initial subscription', async () => {
const key = queryKey()
Expand Down
13 changes: 13 additions & 0 deletions packages/angular-query-experimental/src/create-base-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ export function createBaseQuery<
}),
)

// Subscribing can start a fetch straight away, but the first observer
// notification is batched and only arrives in a later task. Without this
// check the app is considered stable in between, so `whenStable()` resolves
// while the query is still fetching.
untracked(() => {
if (
!pendingTaskRef &&
observer.getCurrentResult().fetchStatus !== 'idle'
) {
pendingTaskRef = pendingTasks.add()
}
})

onCleanup(() => {
if (pendingTaskRef) {
pendingTaskRef()
Expand Down