From 7caab755e9f70636fe44cbb1c80e580315df4abc Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Sun, 31 Dec 2023 02:30:52 +0100 Subject: [PATCH] feat(angular-query): add injectMutationState --- docs/angular/guides/optimistic-updates.md | 1 - .../__tests__/inject-mutation-state.test.ts | 108 ++++++++++++++++++ .../src/__tests__/inject-mutation.test.ts | 15 +-- .../src/create-base-query.ts | 2 + .../src/inject-mutation-state.ts | 77 +++++++++++++ 5 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts create mode 100644 packages/angular-query-experimental/src/inject-mutation-state.ts diff --git a/docs/angular/guides/optimistic-updates.md b/docs/angular/guides/optimistic-updates.md index c10848a367..fdfeb26cf1 100644 --- a/docs/angular/guides/optimistic-updates.md +++ b/docs/angular/guides/optimistic-updates.md @@ -72,7 +72,6 @@ addTodo = injectMutation(() => ({ // access variables somewhere else -// Note: injectMutationState is not available yet in Angular Query mutationState = injectMutationState(() => ({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, diff --git a/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts b/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts new file mode 100644 index 0000000000..2e6a86bc31 --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts @@ -0,0 +1,108 @@ +import { signal } from '@angular/core' +import { QueryClient } from '@tanstack/query-core' +import { TestBed } from '@angular/core/testing' +import { describe, expect, test, vi } from 'vitest' +import { injectMutation } from '../inject-mutation' +import { injectMutationState } from '../inject-mutation-state' +import { provideAngularQuery } from '../providers' +import { successMutator } from './test-utils' + +describe('injectMutationState', () => { + let queryClient: QueryClient + + beforeEach(() => { + queryClient = new QueryClient() + vi.useFakeTimers() + TestBed.configureTestingModule({ + providers: [provideAngularQuery(queryClient)], + }) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + describe('injectMutationState', () => { + test('should return variables after calling mutate', async () => { + const mutationKey = ['mutation'] + const variables = 'foo123' + + const mutation = TestBed.runInInjectionContext(() => { + return injectMutation(() => ({ + mutationKey: mutationKey, + mutationFn: (params: string) => successMutator(params), + })) + }) + + mutation.mutate(variables) + + const mutationState = TestBed.runInInjectionContext(() => { + return injectMutationState(() => ({ + filters: { mutationKey, status: 'pending' }, + select: (m) => m.state.variables, + })) + }) + + expect(mutationState()).toEqual([variables]) + }) + + test('reactive options should update injectMutationState', async () => { + const mutationKey1 = ['mutation1'] + const mutationKey2 = ['mutation2'] + const variables1 = 'foo123' + const variables2 = 'bar234' + + const [mutation1, mutation2] = TestBed.runInInjectionContext(() => { + return [ + injectMutation(() => ({ + mutationKey: mutationKey1, + mutationFn: (params: string) => successMutator(params), + })), + injectMutation(() => ({ + mutationKey: mutationKey2, + mutationFn: (params: string) => successMutator(params), + })), + ] + }) + + mutation1.mutate(variables1) + mutation2.mutate(variables2) + + const filterKey = signal(mutationKey1) + + const mutationState = TestBed.runInInjectionContext(() => { + return injectMutationState(() => ({ + filters: { mutationKey: filterKey(), status: 'pending' }, + select: (m) => m.state.variables, + })) + }) + + expect(mutationState()).toEqual([variables1]) + + filterKey.set(mutationKey2) + TestBed.flushEffects() + expect(mutationState()).toEqual([variables2]) + }) + + test('should return variables after calling mutate', async () => { + queryClient.clear() + const mutationKey = ['mutation'] + const variables = 'bar234' + + const mutation = TestBed.runInInjectionContext(() => { + return injectMutation(() => ({ + mutationKey: mutationKey, + mutationFn: (params: string) => successMutator(params), + })) + }) + + mutation.mutate(variables) + + const mutationState = TestBed.runInInjectionContext(() => { + return injectMutationState() + }) + + expect(mutationState()[0]?.variables).toEqual(variables) + }) + }) +}) diff --git a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts index db718fc301..ebf2390e82 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts @@ -121,7 +121,9 @@ describe('injectMutation', () => { test('reactive options should update mutation', async () => { const mutationCache = queryClient.getMutationCache() - const mutationKey = signal(['foo']) + // Signal will be updated before the mutation is called + // this test confirms that the mutation uses the updated value + const mutationKey = signal(['1']) const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: mutationKey(), @@ -129,19 +131,14 @@ describe('injectMutation', () => { })) }) - mutationKey.set(['bar']) - + mutationKey.set(['2']) TestBed.flushEffects() - await resolveMutations() - mutation.mutate('xyz') - await resolveMutations() - - const mutations = mutationCache.find({ mutationKey: ['bar'] }) + const mutations = mutationCache.find({ mutationKey: ['2'] }) - expect(mutations?.options.mutationKey).toEqual(['bar']) + expect(mutations?.options.mutationKey).toEqual(['2']) }) test('should reset state after invoking mutation.reset', async () => { diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index b12c6f745c..d7c7d39f68 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -72,6 +72,8 @@ export function createBaseQuery< observer.setOptions(defaultedOptions, { listeners: false, }) + // Setting the signal from an effect because it's both 'computed' from options() + // and needs to be set imperatively in the query observer listener. resultSignal.set(observer.getOptimisticResult(defaultedOptions)) }, { allowSignalWrites: true }, diff --git a/packages/angular-query-experimental/src/inject-mutation-state.ts b/packages/angular-query-experimental/src/inject-mutation-state.ts new file mode 100644 index 0000000000..caf52f896a --- /dev/null +++ b/packages/angular-query-experimental/src/inject-mutation-state.ts @@ -0,0 +1,77 @@ +import { DestroyRef, effect, inject, signal } from '@angular/core' +import { + type DefaultError, + type Mutation, + type MutationCache, + type MutationFilters, + type MutationState, + notifyManager, + replaceEqualDeep, +} from '@tanstack/query-core' +import { assertInjector } from './util/assert-injector/assert-injector' +import { injectQueryClient } from './inject-query-client' +import type { Injector, Signal } from '@angular/core' + +type MutationStateOptions = { + filters?: MutationFilters + select?: ( + mutation: Mutation, + ) => TResult +} + +function getResult( + mutationCache: MutationCache, + options: MutationStateOptions, +): Array { + return mutationCache + .findAll(options.filters) + .map( + (mutation): TResult => + (options.select + ? options.select( + mutation as Mutation, + ) + : mutation.state) as TResult, + ) +} + +export function injectMutationState( + options: () => MutationStateOptions = () => ({}), + injector?: Injector, +): Signal> { + return assertInjector(injectMutationState, injector, () => { + const destroyRef = inject(DestroyRef) + const queryClient = injectQueryClient() + + const mutationCache = queryClient.getMutationCache() + + const result = signal>(getResult(mutationCache, options())) + + effect( + () => { + // 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, options())) + }, + { + allowSignalWrites: true, + }, + ) + + const unsubscribe = mutationCache.subscribe( + notifyManager.batchCalls(() => { + const nextResult = replaceEqualDeep( + result(), + getResult(mutationCache, options()), + ) + if (result() !== nextResult) { + result.set(nextResult) + } + }), + ) + + destroyRef.onDestroy(unsubscribe) + + return result + }) +}