diff --git a/.changeset/afraid-plums-shave.md b/.changeset/afraid-plums-shave.md new file mode 100644 index 00000000000..54317645e9a --- /dev/null +++ b/.changeset/afraid-plums-shave.md @@ -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 diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts index 1e7b81ba971..23befc39c2d 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts @@ -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') @@ -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) diff --git a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts index c5a1f58d81f..d722a7982af 100644 --- a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts +++ b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts @@ -1,6 +1,7 @@ import { ApplicationRef, Component, + effect, provideZonelessChangeDetection, } from '@angular/core' import { TestBed } from '@angular/core/testing' @@ -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 = [] + + @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() diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index 4daede76844..d69c9ccad3a 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -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()