11import {
2+ DestroyRef ,
23 Injector ,
34 NgZone ,
45 assertInInjectionContext ,
56 computed ,
6- effect ,
77 inject ,
88 signal ,
99 untracked ,
@@ -18,7 +18,7 @@ import {
1818import { signalProxy } from './signal-proxy'
1919import { PENDING_TASKS } from './pending-tasks-compat'
2020import type { PendingTaskRef } from './pending-tasks-compat'
21- import type { DefaultError , MutationObserverResult } from '@tanstack/query-core'
21+ import type { DefaultError } from '@tanstack/query-core'
2222import type {
2323 CreateMutateFunction ,
2424 CreateMutationOptions ,
@@ -58,6 +58,7 @@ export function injectMutation<
5858) : CreateMutationResult < TData , TError , TVariables , TOnMutateResult > {
5959 ! options ?. injector && assertInInjectionContext ( injectMutation )
6060 const injector = options ?. injector ?? inject ( Injector )
61+ const destroyRef = injector . get ( DestroyRef )
6162 const ngZone = injector . get ( NgZone )
6263 const pendingTasks = injector . get ( PENDING_TASKS )
6364 const queryClient = injector . get ( QueryClient )
@@ -78,7 +79,15 @@ export function injectMutation<
7879 > | null = null
7980
8081 return computed ( ( ) => {
81- return ( instance ||= new MutationObserver ( queryClient , optionsSignal ( ) ) )
82+ const observerOptions = optionsSignal ( )
83+ return untracked ( ( ) => {
84+ if ( instance ) {
85+ instance . setOptions ( observerOptions )
86+ } else {
87+ instance = new MutationObserver ( queryClient , observerOptions )
88+ }
89+ return instance
90+ } )
8291 } )
8392 } ) ( )
8493
@@ -91,93 +100,69 @@ export function injectMutation<
91100 }
92101 } )
93102
94- /**
95- * Computed signal that gets result from mutation cache based on passed options
96- */
97- const resultFromInitialOptionsSignal = computed ( ( ) => {
98- const observer = observerSignal ( )
99- return observer . getCurrentResult ( )
100- } )
103+ let cleanup : ( ) => void = ( ) => { }
101104
102105 /**
103- * Signal that contains result set by subscriber
106+ * Returning a writable signal from a computed is similar to `linkedSignal`,
107+ * but compatible with Angular < 19
108+ *
109+ * Compared to `linkedSignal`, this pattern requires extra parentheses:
110+ * - Accessing value: `result()()`
111+ * - Setting value: `result().set(newValue)`
104112 */
105- const resultFromSubscriberSignal = signal < MutationObserverResult <
106- TData ,
107- TError ,
108- TVariables ,
109- TOnMutateResult
110- > | null > ( null )
113+ const linkedResultSignal = computed ( ( ) => {
114+ const observer = observerSignal ( )
111115
112- effect (
113- ( ) => {
114- const observer = observerSignal ( )
115- const observerOptions = optionsSignal ( )
116+ return untracked ( ( ) => {
117+ const result = signal ( observer . getCurrentResult ( ) )
116118
117- untracked ( ( ) => {
118- observer . setOptions ( observerOptions )
119- } )
120- } ,
121- {
122- injector,
123- } ,
124- )
125-
126- effect (
127- ( onCleanup ) => {
128- // observer.trackResult is not used as this optimization is not needed for Angular
129- const observer = observerSignal ( )
119+ cleanup ( )
130120 let pendingTaskRef : PendingTaskRef | null = null
131121
132- untracked ( ( ) => {
133- const unsubscribe = ngZone . runOutsideAngular ( ( ) =>
134- observer . subscribe (
135- notifyManager . batchCalls ( ( state ) => {
136- ngZone . run ( ( ) => {
137- // Track pending task when mutation is pending
138- if ( state . isPending && ! pendingTaskRef ) {
139- pendingTaskRef = pendingTasks . add ( )
140- }
141-
142- // Clear pending task when mutation is no longer pending
143- if ( ! state . isPending && pendingTaskRef ) {
144- pendingTaskRef ( )
145- pendingTaskRef = null
146- }
147-
148- if (
149- state . isError &&
150- shouldThrowError ( observer . options . throwOnError , [ state . error ] )
151- ) {
152- ngZone . onError . emit ( state . error )
153- throw state . error
154- }
155-
156- resultFromSubscriberSignal . set ( state )
157- } )
158- } ) ,
159- ) ,
160- )
161- onCleanup ( ( ) => {
162- // Clean up any pending task on destroy
163- if ( pendingTaskRef ) {
164- pendingTaskRef ( )
165- pendingTaskRef = null
166- }
167- unsubscribe ( )
168- } )
169- } )
170- } ,
171- {
172- injector,
173- } ,
174- )
122+ const unsubscribe = ngZone . runOutsideAngular ( ( ) =>
123+ observer . subscribe (
124+ notifyManager . batchCalls ( ( state ) => {
125+ ngZone . run ( ( ) => {
126+ // Track pending task when mutation is pending
127+ if ( state . isPending && ! pendingTaskRef ) {
128+ pendingTaskRef = pendingTasks . add ( )
129+ }
130+
131+ // Clear pending task when mutation is no longer pending
132+ if ( ! state . isPending && pendingTaskRef ) {
133+ pendingTaskRef ( )
134+ pendingTaskRef = null
135+ }
136+
137+ if (
138+ state . isError &&
139+ shouldThrowError ( observer . options . throwOnError , [ state . error ] )
140+ ) {
141+ ngZone . onError . emit ( state . error )
142+ throw state . error
143+ }
144+
145+ result . set ( state )
146+ } )
147+ } ) ,
148+ ) ,
149+ )
150+
151+ cleanup = ( ) => {
152+ // Clean up any pending task on destroy
153+ if ( pendingTaskRef ) {
154+ pendingTaskRef ( )
155+ pendingTaskRef = null
156+ }
157+ unsubscribe ( )
158+ }
159+
160+ return result
161+ } )
162+ } )
175163
176164 const resultSignal = computed ( ( ) => {
177- const resultFromSubscriber = resultFromSubscriberSignal ( )
178- const resultFromInitialOptions = resultFromInitialOptionsSignal ( )
179-
180- const result = resultFromSubscriber ?? resultFromInitialOptions
165+ const result = linkedResultSignal ( ) ( )
181166
182167 return {
183168 ...result ,
@@ -186,6 +171,8 @@ export function injectMutation<
186171 }
187172 } )
188173
174+ destroyRef . onDestroy ( cleanup )
175+
189176 return signalProxy ( resultSignal ) as CreateMutationResult <
190177 TData ,
191178 TError ,
0 commit comments