UNPKG

@tanstack/angular-query-experimental

Version:

Signals for managing, caching and syncing asynchronous and remote data in Angular

161 lines (144 loc) 4.42 kB
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 > }) }