@tanstack/angular-query-experimental
Version:
Signals for managing, caching and syncing asynchronous and remote data in Angular
161 lines (144 loc) • 4.42 kB
text/typescript
import {
DestroyRef,
NgZone,
computed,
effect,
inject,
signal,
untracked,
} from '@angular/core'
import {
MutationObserver,
QueryClient,
notifyManager,
} from '@tanstack/query-core'
import { assertInjector } from './util/assert-injector/assert-injector'
import { signalProxy } from './signal-proxy'
import { noop, shouldThrowError } from './util'
import type { Injector } from '@angular/core'
import type { DefaultError, MutationObserverResult } from '@tanstack/query-core'
import type { CreateMutateFunction, CreateMutationResult } from './types'
import type { CreateMutationOptions } from './mutation-options'
/**
* Injects a mutation: an imperative function that can be invoked which typically performs server side effects.
*
* Unlike queries, mutations are not run automatically.
* @param optionsFn - A function that returns mutation options.
* @param injector - The Angular injector to use.
* @returns The mutation.
* @public
*/
export function injectMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
optionsFn: () => CreateMutationOptions<TData, TError, TVariables, TContext>,
injector?: Injector,
): CreateMutationResult<TData, TError, TVariables, TContext> {
return assertInjector(injectMutation, injector, () => {
const destroyRef = inject(DestroyRef)
const ngZone = inject(NgZone)
const queryClient = inject(QueryClient)
/**
* computed() is used so signals can be inserted into the options
* making it reactive. Wrapping options in a function ensures embedded expressions
* are preserved and can keep being applied after signal changes
*/
const optionsSignal = computed(optionsFn)
const observerSignal = (() => {
let instance: MutationObserver<
TData,
TError,
TVariables,
TContext
> | null = null
return computed(() => {
return (instance ||= new MutationObserver(queryClient, optionsSignal()))
})
})()
const mutateFnSignal = computed<
CreateMutateFunction<TData, TError, TVariables, TContext>
>(() => {
const observer = observerSignal()
return (variables, mutateOptions) => {
observer.mutate(variables, mutateOptions).catch(noop)
}
})
/**
* Computed signal that gets result from mutation cache based on passed options
*/
const resultFromInitialOptionsSignal = computed(() => {
const observer = observerSignal()
return observer.getCurrentResult()
})
/**
* Signal that contains result set by subscriber
*/
const resultFromSubscriberSignal = signal<MutationObserverResult<
TData,
TError,
TVariables,
TContext
> | null>(null)
effect(
() => {
const observer = observerSignal()
const options = optionsSignal()
untracked(() => {
observer.setOptions(options)
})
},
{
injector,
},
)
effect(
() => {
// observer.trackResult is not used as this optimization is not needed for Angular
const observer = observerSignal()
untracked(() => {
const unsubscribe = ngZone.runOutsideAngular(() =>
observer.subscribe(
notifyManager.batchCalls((state) => {
ngZone.run(() => {
if (
state.isError &&
shouldThrowError(observer.options.throwOnError, [
state.error,
])
) {
ngZone.onError.emit(state.error)
throw state.error
}
resultFromSubscriberSignal.set(state)
})
}),
),
)
destroyRef.onDestroy(unsubscribe)
})
},
{
injector,
},
)
const resultSignal = computed(() => {
const resultFromSubscriber = resultFromSubscriberSignal()
const resultFromInitialOptions = resultFromInitialOptionsSignal()
const result = resultFromSubscriber ?? resultFromInitialOptions
return {
...result,
mutate: mutateFnSignal(),
mutateAsync: result.mutate,
}
})
return signalProxy(resultSignal) as CreateMutationResult<
TData,
TError,
TVariables,
TContext
>
})
}