From eddedf7cd52f303cb14d6d2a54c773acad6cc529 Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:23:56 +0200 Subject: [PATCH] refactor(angular-query): injectMutationState use signal sandwich pattern --- .changeset/inject-is-mutating-readonly.md | 5 -- .changeset/tidy-parents-send.md | 5 ++ .../src/inject-mutation-state.ts | 70 ++++++------------- 3 files changed, 28 insertions(+), 52 deletions(-) delete mode 100644 .changeset/inject-is-mutating-readonly.md create mode 100644 .changeset/tidy-parents-send.md diff --git a/.changeset/inject-is-mutating-readonly.md b/.changeset/inject-is-mutating-readonly.md deleted file mode 100644 index ce43e97138..0000000000 --- a/.changeset/inject-is-mutating-readonly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@tanstack/angular-query-experimental': patch ---- - -Make `injectIsMutating` signal read-only to prevent external modifications to the internal state \ No newline at end of file diff --git a/.changeset/tidy-parents-send.md b/.changeset/tidy-parents-send.md new file mode 100644 index 0000000000..f57dead760 --- /dev/null +++ b/.changeset/tidy-parents-send.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-query-experimental': patch +--- + +Refactor `injectMutationState` to use signal sandwich pattern for improved consistency and maintainability diff --git a/packages/angular-query-experimental/src/inject-mutation-state.ts b/packages/angular-query-experimental/src/inject-mutation-state.ts index 5f8996b2b1..a45c7e4858 100644 --- a/packages/angular-query-experimental/src/inject-mutation-state.ts +++ b/packages/angular-query-experimental/src/inject-mutation-state.ts @@ -6,12 +6,9 @@ import { computed, inject, signal, + untracked, } from '@angular/core' -import { - QueryClient, - notifyManager, - replaceEqualDeep, -} from '@tanstack/query-core' +import { QueryClient, replaceEqualDeep } from '@tanstack/query-core' import type { Signal } from '@angular/core' import type { Mutation, @@ -53,12 +50,12 @@ export interface InjectMutationStateOptions { /** * Injects a signal that tracks the state of all mutations. - * @param injectMutationStateFn - A function that returns mutation state options. + * @param mutationStateFn - A function that returns mutation state options. * @param options - The Angular injector to use. * @returns The signal that tracks the state of all mutations. */ export function injectMutationState( - injectMutationStateFn: () => MutationStateOptions = () => ({}), + mutationStateFn: () => MutationStateOptions = () => ({}), options?: InjectMutationStateOptions, ): Signal> { !options?.injector && assertInInjectionContext(injectMutationState) @@ -69,53 +66,32 @@ export function injectMutationState( const mutationCache = queryClient.getMutationCache() /** - * Computed signal that gets result from mutation cache based on passed options - * First element is the result, second element is the time when the result was set - */ - const resultFromOptionsSignal = computed(() => { - return [ - getResult(mutationCache, injectMutationStateFn()), - performance.now(), - ] as const - }) - - /** - * Signal that contains result set by subscriber - * First element is the result, second element is the time when the result was set + * Returning a writable signal from a computed is similar to `linkedSignal`, + * but compatible with Angular < 19 + * + * Compared to `linkedSignal`, this pattern requires extra parentheses: + * - Accessing value: `result()()` + * - Setting value: `result().set(newValue)` */ - const resultFromSubscriberSignal = signal<[Array, number] | null>( - null, + const linkedStateSignal = computed(() => + signal(getResult(mutationCache, mutationStateFn())), ) - /** - * Returns the last result by either subscriber or options - */ - const effectiveResultSignal = computed(() => { - const optionsResult = resultFromOptionsSignal() - const subscriberResult = resultFromSubscriberSignal() - return subscriberResult && subscriberResult[1] > optionsResult[1] - ? subscriberResult[0] - : optionsResult[0] - }) + const updateMutationState = () => + ngZone.run(() => + untracked(() => + linkedStateSignal().update((current) => { + const next = getResult(mutationCache, mutationStateFn()) + return replaceEqualDeep(current, next) + }), + ), + ) const unsubscribe = ngZone.runOutsideAngular(() => - mutationCache.subscribe( - notifyManager.batchCalls(() => { - const [lastResult] = effectiveResultSignal() - const nextResult = replaceEqualDeep( - lastResult, - getResult(mutationCache, injectMutationStateFn()), - ) - if (lastResult !== nextResult) { - ngZone.run(() => { - resultFromSubscriberSignal.set([nextResult, performance.now()]) - }) - } - }), - ), + mutationCache.subscribe(updateMutationState), ) destroyRef.onDestroy(unsubscribe) - return effectiveResultSignal + return computed(() => linkedStateSignal()()) }