Skip to content

Commit 5731a3a

Browse files
authored
feat(angular-query): add mutationOptions (#8316)
1 parent 3fa4b7c commit 5731a3a

File tree

8 files changed

+201
-20
lines changed

8 files changed

+201
-20
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@
522522
"label": "Mutations",
523523
"to": "framework/angular/guides/mutations"
524524
},
525+
{
526+
"label": "Mutation Options",
527+
"to": "framework/angular/guides/mutation-options"
528+
},
525529
{
526530
"label": "Query Invalidation",
527531
"to": "framework/angular/guides/query-invalidation"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
id: query-options
3+
title: Mutation Options
4+
---
5+
6+
One of the best ways to share mutation options between multiple places,
7+
is to use the `mutationOptions` helper. At runtime, this helper just returns whatever you pass into it,
8+
but it has a lot of advantages when using it [with TypeScript](../../typescript#typing-query-options).
9+
You can define all possible options for a mutation in one place,
10+
and you'll also get type inference and type safety for all of them.
11+
12+
```ts
13+
export class QueriesService {
14+
private http = inject(HttpClient)
15+
16+
updatePost(id: number) {
17+
return mutationOptions({
18+
mutationFn: (post: Post) => Promise.resolve(post),
19+
mutationKey: ['updatePost', id],
20+
onSuccess: (newPost) => {
21+
// ^? newPost: Post
22+
this.queryClient.setQueryData(['posts', id], newPost)
23+
},
24+
})
25+
}
26+
}
27+
```

docs/framework/angular/typescript.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ replace:
1212
'React Query': 'TanStack Query',
1313
'`success`': '`isSuccess()`',
1414
'function:': 'function.',
15-
'separate function': 'separate function or a service',
1615
}
1716
---
1817

@@ -170,5 +169,90 @@ computed(() => {
170169
```
171170

172171
[//]: # 'RegisterErrorType'
172+
[//]: # 'TypingQueryOptions'
173+
174+
## Typing Query Options
175+
176+
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:
177+
178+
```ts
179+
@Injectable({
180+
providedIn: 'root',
181+
})
182+
export class QueriesService {
183+
private http = inject(HttpClient)
184+
185+
post(postId: number) {
186+
return queryOptions({
187+
queryKey: ['post', postId],
188+
queryFn: () => {
189+
return lastValueFrom(
190+
this.http.get<Post>(
191+
`https://jsonplaceholder.typicode.com/posts/${postId}`,
192+
),
193+
)
194+
},
195+
})
196+
}
197+
}
198+
199+
@Component({
200+
// ...
201+
})
202+
export class Component {
203+
queryClient = inject(QueryClient)
204+
205+
postId = signal(1)
206+
207+
queries = inject(QueriesService)
208+
optionsSignal = computed(() => this.queries.post(this.postId()))
209+
210+
postQuery = injectQuery(() => this.queries.post(1))
211+
postQuery = injectQuery(() => this.queries.post(this.postId()))
212+
213+
// You can also pass a signal which returns query options
214+
postQuery = injectQuery(this.optionsSignal)
215+
216+
someMethod() {
217+
this.queryClient.prefetchQuery(this.queries.post(23))
218+
}
219+
}
220+
```
221+
222+
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:
223+
224+
```ts
225+
data = this.queryClient.getQueryData(groupOptions().queryKey)
226+
// ^? data: Post | undefined
227+
```
228+
229+
Without `queryOptions`, the type of data would be unknown, unless we'd pass a type parameter:
230+
231+
```ts
232+
data = queryClient.getQueryData<Post>(['post', 1])
233+
```
234+
235+
## Typing Mutation Options
236+
237+
Similarly to `queryOptions`, you can use `mutationOptions` to extract mutation options into a separate function:
238+
239+
```ts
240+
export class QueriesService {
241+
private http = inject(HttpClient)
242+
243+
updatePost(id: number) {
244+
return mutationOptions({
245+
mutationFn: (post: Post) => Promise.resolve(post),
246+
mutationKey: ['updatePost', id],
247+
onSuccess: (newPost) => {
248+
// ^? newPost: Post
249+
this.queryClient.setQueryData(['posts', id], newPost)
250+
},
251+
})
252+
}
253+
}
254+
```
255+
256+
[//]: # 'TypingQueryOptions'
173257
[//]: # 'Materials'
174258
[//]: # 'Materials'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { mutationOptions } from '../mutation-options'
2+
3+
describe('mutationOptions', () => {
4+
test('should not allow excess properties', () => {
5+
return mutationOptions({
6+
mutationFn: () => Promise.resolve(5),
7+
mutationKey: ['key'],
8+
// @ts-expect-error this is a good error, because onMutates does not exist!
9+
onMutates: 1000,
10+
})
11+
})
12+
13+
test('should infer types for callbacks', () => {
14+
return mutationOptions({
15+
mutationFn: () => Promise.resolve(5),
16+
mutationKey: ['key'],
17+
onSuccess: (data) => {
18+
expectTypeOf(data).toEqualTypeOf<number>()
19+
},
20+
})
21+
})
22+
})

packages/angular-query-experimental/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type {
1010
UndefinedInitialDataOptions,
1111
} from './query-options'
1212
export { queryOptions } from './query-options'
13+
export { mutationOptions } from './mutation-options'
1314

1415
export type {
1516
DefinedInitialDataInfiniteOptions,

packages/angular-query-experimental/src/inject-mutation.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ import { noop, shouldThrowError } from './util'
1919

2020
import { lazyInit } from './util/lazy-init/lazy-init'
2121
import type { DefaultError, MutationObserverResult } from '@tanstack/query-core'
22-
import type {
23-
CreateMutateFunction,
24-
CreateMutationOptions,
25-
CreateMutationResult,
26-
} from './types'
22+
import type { CreateMutateFunction, CreateMutationResult } from './types'
23+
import type { CreateMutationOptions } from './mutation-options'
2724

2825
/**
2926
* Injects a mutation: an imperative function that can be invoked which typically performs server side effects.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type {
2+
DefaultError,
3+
MutationObserverOptions,
4+
OmitKeyof,
5+
} from '@tanstack/query-core'
6+
7+
/**
8+
* Allows to share and re-use mutation options in a type-safe way.
9+
*
10+
* **Example**
11+
*
12+
* ```ts
13+
* export class QueriesService {
14+
* private http = inject(HttpClient);
15+
*
16+
* updatePost(id: number) {
17+
* return mutationOptions({
18+
* mutationFn: (post: Post) => Promise.resolve(post),
19+
* mutationKey: ["updatePost", id],
20+
* onSuccess: (newPost) => {
21+
* // ^? newPost: Post
22+
* this.queryClient.setQueryData(["posts", id], newPost);
23+
* },
24+
* });
25+
* }
26+
* }
27+
*
28+
* queries = inject(QueriesService)
29+
* idSignal = new Signal(0);
30+
* mutation = injectMutation(() => this.queries.updatePost(this.idSignal()))
31+
*
32+
* mutation.mutate({ title: 'New Title' })
33+
* ```
34+
* @param options - The mutation options.
35+
* @returns Mutation options.
36+
* @public
37+
*/
38+
export function mutationOptions<
39+
TData = unknown,
40+
TError = DefaultError,
41+
TVariables = void,
42+
TContext = unknown,
43+
>(
44+
options: MutationObserverOptions<TData, TError, TVariables, TContext>,
45+
): CreateMutationOptions<TData, TError, TVariables, TContext> {
46+
return options
47+
}
48+
49+
/**
50+
* @public
51+
*/
52+
export interface CreateMutationOptions<
53+
TData = unknown,
54+
TError = DefaultError,
55+
TVariables = void,
56+
TContext = unknown,
57+
> extends OmitKeyof<
58+
MutationObserverOptions<TData, TError, TVariables, TContext>,
59+
'_defaulted'
60+
> {}

packages/angular-query-experimental/src/types.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type {
77
InfiniteQueryObserverOptions,
88
InfiniteQueryObserverResult,
99
MutateFunction,
10-
MutationObserverOptions,
1110
MutationObserverResult,
1211
OmitKeyof,
1312
Override,
@@ -159,19 +158,6 @@ export type DefinedCreateInfiniteQueryResult<
159158
>,
160159
> = MapToSignals<TDefinedInfiniteQueryObserver>
161160

162-
/**
163-
* @public
164-
*/
165-
export interface CreateMutationOptions<
166-
TData = unknown,
167-
TError = DefaultError,
168-
TVariables = void,
169-
TContext = unknown,
170-
> extends OmitKeyof<
171-
MutationObserverOptions<TData, TError, TVariables, TContext>,
172-
'_defaulted'
173-
> {}
174-
175161
/**
176162
* @public
177163
*/

0 commit comments

Comments
 (0)