Skip to content

Commit

Permalink
feat(angular-query): add mutationOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
arnoud-dv committed Nov 20, 2024
1 parent 6a9edbf commit 2b22cdf
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 20 deletions.
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions docs/framework/angular/guides/mutation-options.md
Original file line number Diff line number Diff line change
@@ -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)
},
})
}
}
```
86 changes: 85 additions & 1 deletion docs/framework/angular/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ replace:
'React Query': 'TanStack Query',
'`success`': '`isSuccess()`',
'function:': 'function.',
'separate function': 'separate function or a service',
}
---

Expand Down Expand Up @@ -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<Post>(
`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>(['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'
Original file line number Diff line number Diff line change
@@ -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<number>()
},
})
})
})
1 change: 1 addition & 0 deletions packages/angular-query-experimental/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type {
UndefinedInitialDataOptions,
} from './query-options'
export { queryOptions } from './query-options'
export { mutationOptions } from './mutation-options'

export type {
DefinedInitialDataInfiniteOptions,
Expand Down
7 changes: 2 additions & 5 deletions packages/angular-query-experimental/src/inject-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
60 changes: 60 additions & 0 deletions packages/angular-query-experimental/src/mutation-options.ts
Original file line number Diff line number Diff line change
@@ -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<TData, TError, TVariables, TContext>,
): CreateMutationOptions<TData, TError, TVariables, TContext> {
return options

Check warning on line 46 in packages/angular-query-experimental/src/mutation-options.ts

View check run for this annotation

Codecov / codecov/patch

packages/angular-query-experimental/src/mutation-options.ts#L45-L46

Added lines #L45 - L46 were not covered by tests
}

/**
* @public
*/
export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> extends OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TContext>,
'_defaulted'
> {}
14 changes: 0 additions & 14 deletions packages/angular-query-experimental/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
MutateFunction,
MutationObserverOptions,
MutationObserverResult,
OmitKeyof,
Override,
Expand Down Expand Up @@ -159,19 +158,6 @@ export type DefinedCreateInfiniteQueryResult<
>,
> = MapToSignals<TDefinedInfiniteQueryObserver>

/**
* @public
*/
export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> extends OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TContext>,
'_defaulted'
> {}

/**
* @public
*/
Expand Down

0 comments on commit 2b22cdf

Please sign in to comment.