From 2b22cdfe66ce46f257d93db30b1fc892d78b50d4 Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:11:52 +0100 Subject: [PATCH] feat(angular-query): add mutationOptions --- docs/config.json | 4 + .../angular/guides/mutation-options.md | 27 ++++++ docs/framework/angular/typescript.md | 86 ++++++++++++++++++- .../src/__tests__/mutation-options.test-d.ts | 22 +++++ .../angular-query-experimental/src/index.ts | 1 + .../src/inject-mutation.ts | 7 +- .../src/mutation-options.ts | 60 +++++++++++++ .../angular-query-experimental/src/types.ts | 14 --- 8 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 docs/framework/angular/guides/mutation-options.md create mode 100644 packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts create mode 100644 packages/angular-query-experimental/src/mutation-options.ts diff --git a/docs/config.json b/docs/config.json index 943e3cbfa1..ef151ed38f 100644 --- a/docs/config.json +++ b/docs/config.json @@ -522,6 +522,10 @@ "label": "Mutations", "to": "framework/angular/guides/mutations" }, + { + "label": "Mutation Options", + "to": "framework/angular/guides/mutation-options" + }, { "label": "Query Invalidation", "to": "framework/angular/guides/query-invalidation" diff --git a/docs/framework/angular/guides/mutation-options.md b/docs/framework/angular/guides/mutation-options.md new file mode 100644 index 0000000000..8fd372a0ba --- /dev/null +++ b/docs/framework/angular/guides/mutation-options.md @@ -0,0 +1,27 @@ +--- +id: query-options +title: Mutation Options +--- + +One of the best ways to share mutation options between multiple places, +is to use the `mutationOptions` helper. At runtime, this helper just returns whatever you pass into it, +but it has a lot of advantages when using it [with TypeScript](../../typescript#typing-query-options). +You can define all possible options for a mutation in one place, +and you'll also get type inference and type safety for all of them. + +```ts +export class QueriesService { + private http = inject(HttpClient) + + updatePost(id: number) { + return mutationOptions({ + mutationFn: (post: Post) => Promise.resolve(post), + mutationKey: ['updatePost', id], + onSuccess: (newPost) => { + // ^? newPost: Post + this.queryClient.setQueryData(['posts', id], newPost) + }, + }) + } +} +``` diff --git a/docs/framework/angular/typescript.md b/docs/framework/angular/typescript.md index 3ca573badc..6a10e940bd 100644 --- a/docs/framework/angular/typescript.md +++ b/docs/framework/angular/typescript.md @@ -12,7 +12,6 @@ replace: 'React Query': 'TanStack Query', '`success`': '`isSuccess()`', 'function:': 'function.', - 'separate function': 'separate function or a service', } --- @@ -170,5 +169,90 @@ computed(() => { ``` [//]: # 'RegisterErrorType' +[//]: # 'TypingQueryOptions' + +## Typing Query Options + +If you inline query options into `injectQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `injectQuery` and e.g. `prefetchQuery` or manage them in a service. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper: + +```ts +@Injectable({ + providedIn: 'root', +}) +export class QueriesService { + private http = inject(HttpClient) + + post(postId: number) { + return queryOptions({ + queryKey: ['post', postId], + queryFn: () => { + return lastValueFrom( + this.http.get( + `https://jsonplaceholder.typicode.com/posts/${postId}`, + ), + ) + }, + }) + } +} + +@Component({ + // ... +}) +export class Component { + queryClient = inject(QueryClient) + + postId = signal(1) + + queries = inject(QueriesService) + optionsSignal = computed(() => this.queries.post(this.postId())) + + postQuery = injectQuery(() => this.queries.post(1)) + postQuery = injectQuery(() => this.queries.post(this.postId())) + + // You can also pass a signal which returns query options + postQuery = injectQuery(this.optionsSignal) + + someMethod() { + this.queryClient.prefetchQuery(this.queries.post(23)) + } +} +``` + +Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well: + +```ts +data = this.queryClient.getQueryData(groupOptions().queryKey) +// ^? data: Post | undefined +``` + +Without `queryOptions`, the type of data would be unknown, unless we'd pass a type parameter: + +```ts +data = queryClient.getQueryData(['post', 1]) +``` + +## Typing Mutation Options + +Similarly to `queryOptions`, you can use `mutationOptions` to extract mutation options into a separate function: + +```ts +export class QueriesService { + private http = inject(HttpClient) + + updatePost(id: number) { + return mutationOptions({ + mutationFn: (post: Post) => Promise.resolve(post), + mutationKey: ['updatePost', id], + onSuccess: (newPost) => { + // ^? newPost: Post + this.queryClient.setQueryData(['posts', id], newPost) + }, + }) + } +} +``` + +[//]: # 'TypingQueryOptions' [//]: # 'Materials' [//]: # 'Materials' diff --git a/packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts b/packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts new file mode 100644 index 0000000000..0aea5556a8 --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts @@ -0,0 +1,22 @@ +import { mutationOptions } from '../mutation-options' + +describe('mutationOptions', () => { + test('should not allow excess properties', () => { + return mutationOptions({ + mutationFn: () => Promise.resolve(5), + mutationKey: ['key'], + // @ts-expect-error this is a good error, because onMutates does not exist! + onMutates: 1000, + }) + }) + + test('should infer types for callbacks', () => { + return mutationOptions({ + mutationFn: () => Promise.resolve(5), + mutationKey: ['key'], + onSuccess: (data) => { + expectTypeOf(data).toEqualTypeOf() + }, + }) + }) +}) diff --git a/packages/angular-query-experimental/src/index.ts b/packages/angular-query-experimental/src/index.ts index b4db6cd11e..794bff747e 100644 --- a/packages/angular-query-experimental/src/index.ts +++ b/packages/angular-query-experimental/src/index.ts @@ -10,6 +10,7 @@ export type { UndefinedInitialDataOptions, } from './query-options' export { queryOptions } from './query-options' +export { mutationOptions } from './mutation-options' export type { DefinedInitialDataInfiniteOptions, diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index 134ce19c02..9320c7afb2 100644 --- a/packages/angular-query-experimental/src/inject-mutation.ts +++ b/packages/angular-query-experimental/src/inject-mutation.ts @@ -19,11 +19,8 @@ import { noop, shouldThrowError } from './util' import { lazyInit } from './util/lazy-init/lazy-init' import type { DefaultError, MutationObserverResult } from '@tanstack/query-core' -import type { - CreateMutateFunction, - CreateMutationOptions, - CreateMutationResult, -} from './types' +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. diff --git a/packages/angular-query-experimental/src/mutation-options.ts b/packages/angular-query-experimental/src/mutation-options.ts new file mode 100644 index 0000000000..b55f0367db --- /dev/null +++ b/packages/angular-query-experimental/src/mutation-options.ts @@ -0,0 +1,60 @@ +import type { + DefaultError, + MutationObserverOptions, + OmitKeyof, +} from '@tanstack/query-core' + +/** + * Allows to share and re-use mutation options in a type-safe way. + * + * **Example** + * + * ```ts + * export class QueriesService { + * private http = inject(HttpClient); + * + * updatePost(id: number) { + * return mutationOptions({ + * mutationFn: (post: Post) => Promise.resolve(post), + * mutationKey: ["updatePost", id], + * onSuccess: (newPost) => { + * // ^? newPost: Post + * this.queryClient.setQueryData(["posts", id], newPost); + * }, + * }); + * } + * } + * + * queries = inject(QueriesService) + * idSignal = new Signal(0); + * mutation = injectMutation(() => this.queries.updatePost(this.idSignal())) + * + * mutation.mutate({ title: 'New Title' }) + * ``` + * @param options - The mutation options. + * @returns Mutation options. + * @public + */ +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TContext = unknown, +>( + options: MutationObserverOptions, +): CreateMutationOptions { + return options +} + +/** + * @public + */ +export interface CreateMutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TContext = unknown, +> extends OmitKeyof< + MutationObserverOptions, + '_defaulted' + > {} diff --git a/packages/angular-query-experimental/src/types.ts b/packages/angular-query-experimental/src/types.ts index 2211d3b6f7..5a5262caad 100644 --- a/packages/angular-query-experimental/src/types.ts +++ b/packages/angular-query-experimental/src/types.ts @@ -7,7 +7,6 @@ import type { InfiniteQueryObserverOptions, InfiniteQueryObserverResult, MutateFunction, - MutationObserverOptions, MutationObserverResult, OmitKeyof, Override, @@ -159,19 +158,6 @@ export type DefinedCreateInfiniteQueryResult< >, > = MapToSignals -/** - * @public - */ -export interface CreateMutationOptions< - TData = unknown, - TError = DefaultError, - TVariables = void, - TContext = unknown, -> extends OmitKeyof< - MutationObserverOptions, - '_defaulted' - > {} - /** * @public */