0
# Mutation Management
1
2
Mutation functionality for server-side effects with optimistic updates, error handling, and automatic query invalidation using Angular signals.
3
4
## Capabilities
5
6
### Inject Mutation
7
8
Creates a mutation that can be executed to perform server-side effects like creating, updating, or deleting data.
9
10
```typescript { .api }
11
/**
12
* Injects a mutation: an imperative function that can be invoked which typically performs server side effects.
13
* Unlike queries, mutations are not run automatically.
14
* @param injectMutationFn - A function that returns mutation options
15
* @param options - Additional configuration including custom injector
16
* @returns The mutation result with signals and mutate functions
17
*/
18
function injectMutation<TData, TError, TVariables, TContext>(
19
injectMutationFn: () => CreateMutationOptions<TData, TError, TVariables, TContext>,
20
options?: InjectMutationOptions
21
): CreateMutationResult<TData, TError, TVariables, TContext>;
22
```
23
24
**Usage Examples:**
25
26
```typescript
27
import { injectMutation, QueryClient } from "@tanstack/angular-query-experimental";
28
import { Component, inject } from "@angular/core";
29
import { HttpClient } from "@angular/common/http";
30
31
@Component({
32
selector: 'app-user-form',
33
template: `
34
<form (ngSubmit)="handleSubmit()">
35
<input [(ngModel)]="name" placeholder="Name" />
36
<input [(ngModel)]="email" placeholder="Email" />
37
<button
38
type="submit"
39
[disabled]="createUserMutation.isPending()"
40
>
41
{{ createUserMutation.isPending() ? 'Creating...' : 'Create User' }}
42
</button>
43
</form>
44
45
<div *ngIf="createUserMutation.isError()">
46
Error: {{ createUserMutation.error()?.message }}
47
</div>
48
49
<div *ngIf="createUserMutation.isSuccess()">
50
User created: {{ createUserMutation.data()?.name }}
51
</div>
52
`
53
})
54
export class UserFormComponent {
55
#http = inject(HttpClient);
56
#queryClient = inject(QueryClient);
57
58
name = '';
59
email = '';
60
61
// Basic mutation
62
createUserMutation = injectMutation(() => ({
63
mutationFn: (userData: CreateUserRequest) =>
64
this.#http.post<User>('/api/users', userData),
65
onSuccess: (newUser) => {
66
// Invalidate and refetch users list
67
this.#queryClient.invalidateQueries({ queryKey: ['users'] });
68
// Optionally set data directly
69
this.#queryClient.setQueryData(['user', newUser.id], newUser);
70
},
71
onError: (error) => {
72
console.error('Failed to create user:', error);
73
}
74
}));
75
76
// Mutation with optimistic updates
77
updateUserMutation = injectMutation(() => ({
78
mutationFn: (data: { id: number; updates: Partial<User> }) =>
79
this.#http.patch<User>(`/api/users/${data.id}`, data.updates),
80
onMutate: async (variables) => {
81
// Cancel outgoing refetches
82
await this.#queryClient.cancelQueries({ queryKey: ['user', variables.id] });
83
84
// Snapshot previous value
85
const previousUser = this.#queryClient.getQueryData<User>(['user', variables.id]);
86
87
// Optimistically update
88
this.#queryClient.setQueryData(['user', variables.id], (old: User) => ({
89
...old,
90
...variables.updates
91
}));
92
93
return { previousUser };
94
},
95
onError: (error, variables, context) => {
96
// Rollback on error
97
if (context?.previousUser) {
98
this.#queryClient.setQueryData(['user', variables.id], context.previousUser);
99
}
100
},
101
onSettled: (data, error, variables) => {
102
// Always refetch after error or success
103
this.#queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
104
}
105
}));
106
107
handleSubmit() {
108
this.createUserMutation.mutate({
109
name: this.name,
110
email: this.email
111
});
112
}
113
}
114
115
interface User {
116
id: number;
117
name: string;
118
email: string;
119
}
120
121
interface CreateUserRequest {
122
name: string;
123
email: string;
124
}
125
```
126
127
### Mutation Options Interface
128
129
Comprehensive options for configuring mutation behavior.
130
131
```typescript { .api }
132
interface CreateMutationOptions<TData, TError, TVariables, TContext> {
133
/** Function that performs the mutation and returns a promise */
134
mutationFn: MutationFunction<TData, TVariables>;
135
/** Unique key for the mutation (optional) */
136
mutationKey?: MutationKey;
137
/** Called before mutation function is fired */
138
onMutate?: (variables: TVariables) => Promise<TContext> | TContext;
139
/** Called on successful mutation */
140
onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<void> | void;
141
/** Called on mutation error */
142
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<void> | void;
143
/** Called after mutation completes (success or error) */
144
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<void> | void;
145
/** Number of retry attempts on failure */
146
retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
147
/** Delay function for retry attempts */
148
retryDelay?: number | ((retryAttempt: number, error: TError) => number);
149
/** Whether to throw errors instead of setting error state */
150
throwOnError?: boolean | ((error: TError) => boolean);
151
/** Time in milliseconds after which unused mutation data is garbage collected */
152
gcTime?: number;
153
/** Custom meta information */
154
meta?: Record<string, unknown>;
155
/** Whether to use network mode */
156
networkMode?: 'online' | 'always' | 'offlineFirst';
157
}
158
```
159
160
### Mutation Result Interface
161
162
Signal-based result object providing reactive access to mutation state.
163
164
```typescript { .api }
165
interface CreateMutationResult<TData, TError, TVariables, TContext> {
166
/** Function to trigger the mutation */
167
mutate: CreateMutateFunction<TData, TError, TVariables, TContext>;
168
/** Async function to trigger the mutation and return a promise */
169
mutateAsync: CreateMutateAsyncFunction<TData, TError, TVariables, TContext>;
170
/** Signal containing the mutation data */
171
data: Signal<TData | undefined>;
172
/** Signal containing any error that occurred */
173
error: Signal<TError | null>;
174
/** Signal containing the variables passed to the mutation */
175
variables: Signal<TVariables | undefined>;
176
/** Signal indicating if mutation is currently running */
177
isPending: Signal<boolean>;
178
/** Signal indicating if mutation completed successfully */
179
isSuccess: Signal<boolean>;
180
/** Signal indicating if mutation resulted in error */
181
isError: Signal<boolean>;
182
/** Signal indicating if mutation is idle (not yet called) */
183
isIdle: Signal<boolean>;
184
/** Signal containing mutation status */
185
status: Signal<'idle' | 'pending' | 'error' | 'success'>;
186
/** Signal containing current failure count */
187
failureCount: Signal<number>;
188
/** Signal containing failure reason */
189
failureReason: Signal<TError | null>;
190
191
// Type narrowing methods
192
isSuccess(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
193
isError(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
194
isPending(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
195
isIdle(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;
196
}
197
```
198
199
### Mutation Function Types
200
201
Type definitions for mutation functions.
202
203
```typescript { .api }
204
type CreateMutateFunction<TData, TError, TVariables, TContext> = (
205
variables: TVariables,
206
options?: {
207
onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;
208
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void;
209
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void;
210
}
211
) => void;
212
213
type CreateMutateAsyncFunction<TData, TError, TVariables, TContext> = (
214
variables: TVariables,
215
options?: {
216
onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;
217
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void;
218
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void;
219
}
220
) => Promise<TData>;
221
222
type MutationFunction<TData, TVariables> = (variables: TVariables) => Promise<TData>;
223
```
224
225
### Options Configuration
226
227
Configuration interface for injectMutation behavior.
228
229
```typescript { .api }
230
interface InjectMutationOptions {
231
/**
232
* The Injector in which to create the mutation.
233
* If not provided, the current injection context will be used instead (via inject).
234
*/
235
injector?: Injector;
236
}
237
```
238
239
## Advanced Usage Patterns
240
241
### Optimistic Updates
242
243
```typescript
244
@Component({})
245
export class OptimisticUpdateComponent {
246
#http = inject(HttpClient);
247
#queryClient = inject(QueryClient);
248
249
updateTodoMutation = injectMutation(() => ({
250
mutationFn: (data: { id: number; completed: boolean }) =>
251
this.#http.patch<Todo>(`/api/todos/${data.id}`, { completed: data.completed }),
252
253
onMutate: async (variables) => {
254
// Cancel any outgoing refetches so they don't overwrite optimistic update
255
await this.#queryClient.cancelQueries({ queryKey: ['todos'] });
256
257
// Snapshot previous value
258
const previousTodos = this.#queryClient.getQueryData<Todo[]>(['todos']);
259
260
// Optimistically update the cache
261
this.#queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
262
old.map(todo =>
263
todo.id === variables.id
264
? { ...todo, completed: variables.completed }
265
: todo
266
)
267
);
268
269
return { previousTodos };
270
},
271
272
onError: (error, variables, context) => {
273
// Rollback to previous state on error
274
if (context?.previousTodos) {
275
this.#queryClient.setQueryData(['todos'], context.previousTodos);
276
}
277
},
278
279
onSettled: () => {
280
// Always refetch after error or success to ensure we have latest data
281
this.#queryClient.invalidateQueries({ queryKey: ['todos'] });
282
}
283
}));
284
}
285
```
286
287
### Sequential Mutations
288
289
```typescript
290
@Component({})
291
export class SequentialMutationsComponent {
292
#http = inject(HttpClient);
293
#queryClient = inject(QueryClient);
294
295
createUserAndProfileMutation = injectMutation(() => ({
296
mutationFn: async (userData: CreateUserData) => {
297
// First create the user
298
const user = await this.#http.post<User>('/api/users', {
299
name: userData.name,
300
email: userData.email
301
}).toPromise();
302
303
// Then create their profile
304
const profile = await this.#http.post<Profile>('/api/profiles', {
305
userId: user.id,
306
bio: userData.bio,
307
avatar: userData.avatar
308
}).toPromise();
309
310
return { user, profile };
311
},
312
313
onSuccess: ({ user, profile }) => {
314
// Update cache with both entities
315
this.#queryClient.setQueryData(['user', user.id], user);
316
this.#queryClient.setQueryData(['profile', user.id], profile);
317
this.#queryClient.invalidateQueries({ queryKey: ['users'] });
318
}
319
}));
320
}
321
```
322
323
### Error Recovery
324
325
```typescript
326
@Component({})
327
export class ErrorRecoveryComponent {
328
#http = inject(HttpClient);
329
330
retryableMutation = injectMutation(() => ({
331
mutationFn: (data: any) => this.#http.post('/api/data', data),
332
333
retry: (failureCount, error: any) => {
334
// Retry up to 3 times, but only for specific errors
335
if (failureCount < 3) {
336
// Retry on network errors or 5xx server errors
337
return !error.status || error.status >= 500;
338
}
339
return false;
340
},
341
342
retryDelay: (attemptIndex) => {
343
// Exponential backoff with jitter
344
const baseDelay = Math.min(1000 * 2 ** attemptIndex, 30000);
345
return baseDelay + Math.random() * 1000;
346
},
347
348
onError: (error, variables, context) => {
349
// Log error for monitoring
350
console.error('Mutation failed after retries:', error);
351
352
// Could show user-friendly error message
353
this.showErrorMessage(error);
354
}
355
}));
356
357
private showErrorMessage(error: any) {
358
// Implementation for showing user-friendly errors
359
}
360
}
361
```
362
363
### Global Mutation Defaults
364
365
```typescript
366
// Can be set up in app configuration
367
const queryClient = new QueryClient({
368
defaultOptions: {
369
mutations: {
370
retry: 1,
371
throwOnError: false,
372
onError: (error) => {
373
// Global error handling for all mutations
374
console.error('Mutation error:', error);
375
}
376
}
377
}
378
});
379
```