diff --git a/packages/angular-query-experimental/src/__tests__/test-utils.ts b/packages/angular-query-experimental/src/__tests__/test-utils.ts index 1f9f6d9b25..d729cf0d4a 100644 --- a/packages/angular-query-experimental/src/__tests__/test-utils.ts +++ b/packages/angular-query-experimental/src/__tests__/test-utils.ts @@ -125,3 +125,7 @@ export function setFixtureSignalInputs>( componentFixture.detectChanges() } } + +export async function flushQueue() { + await new Promise(setImmediate) +} diff --git a/packages/angular-query-experimental/src/__tests__/util/lazy-init/lazy-init.test.ts b/packages/angular-query-experimental/src/__tests__/util/lazy-init/lazy-init.test.ts new file mode 100644 index 0000000000..2cf6b2740d --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/util/lazy-init/lazy-init.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, test } from 'vitest' +import { + ChangeDetectionStrategy, + Component, + type WritableSignal, + computed, + effect, + input, + signal, +} from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { flushQueue, setFixtureSignalInputs } from '../../test-utils' +import { lazyInit } from '../../../util/lazy-init/lazy-init' + +describe('lazyInit', () => { + test('should init lazily in next tick when not accessing manually', async () => { + const mockFn = vi.fn() + + TestBed.runInInjectionContext(() => { + lazyInit(() => { + mockFn() + return { + data: signal(true), + } + }) + }) + + expect(mockFn).not.toHaveBeenCalled() + + await new Promise(setImmediate) + + expect(mockFn).toHaveBeenCalled() + }) + + test('should init eagerly accessing manually', async () => { + const mockFn = vi.fn() + + TestBed.runInInjectionContext(() => { + const lazySignal = lazyInit(() => { + mockFn() + return { + data: signal(true), + } + }) + + lazySignal.data() + + console.log(lazySignal) + }) + + expect(mockFn).toHaveBeenCalled() + }) + + test('should init lazily and only once', async () => { + const initCallFn = vi.fn() + const registerDataValue = vi.fn<[number]>() + + let value!: { data: WritableSignal } + const outerSignal = signal(0) + + TestBed.runInInjectionContext(() => { + value = lazyInit(() => { + initCallFn() + + void outerSignal() + + return { data: signal(0) } + }) + + effect(() => registerDataValue(value.data())) + }) + + value.data() + + await flushQueue() + + expect(outerSignal).toBeDefined() + + expect(initCallFn).toHaveBeenCalledTimes(1) + + outerSignal.set(1) + await flushQueue() + outerSignal.set(2) + await flushQueue() + value.data.set(4) + await flushQueue() + + expect(initCallFn).toHaveBeenCalledTimes(1) + expect(registerDataValue).toHaveBeenCalledTimes(2) + }) + + test('should support required signal input', async () => { + @Component({ + standalone: true, + template: `{{ call }} - {{ lazySignal.data() }}`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Test { + readonly title = input.required() + call = 0 + + lazySignal = lazyInit(() => { + this.call++ + return { + data: computed(() => this.title()), + } + }) + } + + const fixture = TestBed.createComponent(Test) + + setFixtureSignalInputs(fixture, { title: 'newValue' }) + expect(fixture.debugElement.nativeElement.textContent).toBe('0 - newValue') + await flushQueue() + + setFixtureSignalInputs(fixture, { title: 'updatedValue' }) + expect(fixture.debugElement.nativeElement.textContent).toBe( + '1 - updatedValue', + ) + + setFixtureSignalInputs(fixture, { title: 'newUpdatedValue' }) + expect(fixture.debugElement.nativeElement.textContent).toBe( + '1 - newUpdatedValue', + ) + }) +}) diff --git a/packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts b/packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts index afdd71e3f4..26b5965cf2 100644 --- a/packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts +++ b/packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts @@ -9,11 +9,7 @@ import { } from '@angular/core' import { TestBed } from '@angular/core/testing' import { lazySignalInitializer } from '../../../util/lazy-signal-initializer/lazy-signal-initializer' -import { setFixtureSignalInputs } from '../../test-utils' - -async function flushQueue() { - await new Promise(setImmediate) -} +import { flushQueue, setFixtureSignalInputs } from '../../test-utils' describe('lazySignalInitializer', () => { test('should init lazily in next tick when not accessing manually', async () => { @@ -69,9 +65,7 @@ describe('lazySignalInitializer', () => { effect(() => registerEffectValue(value())) }) - expect(initCallFn).toHaveBeenCalledTimes(0) - expect(outerSignal).toBeDefined() - expect(innerSignal).not.toBeDefined() + value() await flushQueue() diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index a6685da626..2b75d2a66c 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -10,7 +10,7 @@ import { } from '@angular/core' import { notifyManager } from '@tanstack/query-core' import { signalProxy } from './signal-proxy' -import { lazyInit } from './lazy-init' +import { lazyInit } from './util/lazy-init/lazy-init' import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core' import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types' @@ -67,20 +67,17 @@ export function createBaseQuery< observer.getOptimisticResult(defaultedOptionsSignal()), ) - // Effects should not be called inside reactive contexts - untracked(() => - effect(() => { - const defaultedOptions = defaultedOptionsSignal() - observer.setOptions(defaultedOptions, { - // Do not notify on updates because of changes in the options because - // these changes should already be reflected in the optimistic result. - listeners: false, - }) - untracked(() => { - resultSignal.set(observer.getOptimisticResult(defaultedOptions)) - }) - }), - ) + effect(() => { + const defaultedOptions = defaultedOptionsSignal() + observer.setOptions(defaultedOptions, { + // Do not notify on updates because of changes in the options because + // these changes should already be reflected in the optimistic result. + listeners: false, + }) + untracked(() => { + resultSignal.set(observer.getOptimisticResult(defaultedOptions)) + }) + }) // observer.trackResult is not used as this optimization is not needed for Angular const unsubscribe = observer.subscribe( diff --git a/packages/angular-query-experimental/src/inject-mutation-state.ts b/packages/angular-query-experimental/src/inject-mutation-state.ts index 1de27d2d40..5f516ffe71 100644 --- a/packages/angular-query-experimental/src/inject-mutation-state.ts +++ b/packages/angular-query-experimental/src/inject-mutation-state.ts @@ -55,19 +55,17 @@ export function injectMutationState( getResult(mutationCache, mutationStateOptionsFn()), ) - untracked(() => { - effect( - () => { - const mutationStateOptions = mutationStateOptionsFn() - untracked(() => { - // Setting the signal from an effect because it's both 'computed' from options() - // and needs to be set imperatively in the mutationCache listener. - result.set(getResult(mutationCache, mutationStateOptions)) - }) - }, - { injector }, - ) - }) + effect( + () => { + const mutationStateOptions = mutationStateOptionsFn() + untracked(() => { + // Setting the signal from an effect because it's both 'computed' from options() + // and needs to be set imperatively in the mutationCache listener. + result.set(getResult(mutationCache, mutationStateOptions)) + }) + }, + { injector }, + ) const unsubscribe = mutationCache.subscribe( notifyManager.batchCalls(() => { diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index cd731189ae..3ab52f1736 100644 --- a/packages/angular-query-experimental/src/inject-mutation.ts +++ b/packages/angular-query-experimental/src/inject-mutation.ts @@ -6,7 +6,6 @@ import { inject, runInInjectionContext, signal, - untracked, } from '@angular/core' import { MutationObserver, notifyManager } from '@tanstack/query-core' import { assertInjector } from './util/assert-injector/assert-injector' @@ -14,7 +13,7 @@ import { signalProxy } from './signal-proxy' import { injectQueryClient } from './inject-query-client' import { noop } from './util' -import { lazyInit } from './lazy-init' +import { lazyInit } from './util/lazy-init/lazy-init' import type { DefaultError, QueryClient } from '@tanstack/query-core' import type { CreateMutateFunction, @@ -55,11 +54,8 @@ export function injectMutation< observer.mutate(variables, mutateOptions).catch(noop) } - // Effects should not be called inside reactive contexts - untracked(() => { - effect(() => { - observer.setOptions(options(queryClient)) - }) + effect(() => { + observer.setOptions(options(queryClient)) }) const result = signal(observer.getCurrentResult()) diff --git a/packages/angular-query-experimental/src/lazy-init.ts b/packages/angular-query-experimental/src/util/lazy-init/lazy-init.ts similarity index 88% rename from packages/angular-query-experimental/src/lazy-init.ts rename to packages/angular-query-experimental/src/util/lazy-init/lazy-init.ts index 86b2a6f0ca..16c58429c3 100644 --- a/packages/angular-query-experimental/src/lazy-init.ts +++ b/packages/angular-query-experimental/src/util/lazy-init/lazy-init.ts @@ -1,9 +1,11 @@ +import { untracked } from '@angular/core' + export function lazyInit(initializer: () => T): T { let object: T | null = null const initializeObject = () => { if (!object) { - object = initializer() + object = untracked(() => initializer()) } }