0
# Mutations
1
2
Mutation management for data modifications with optimistic updates, rollback capabilities, side effect handling, and automatic query invalidation.
3
4
## Capabilities
5
6
### MutationObserver
7
8
Observer for tracking and executing mutations with automatic state management.
9
10
```typescript { .api }
11
/**
12
* Observer for managing mutations and tracking their state
13
* Provides reactive updates for mutation progress, success, and error states
14
*/
15
class MutationObserver<
16
TData = unknown,
17
TError = Error,
18
TVariables = void,
19
TContext = unknown
20
> {
21
constructor(client: QueryClient, options: MutationObserverOptions<TData, TError, TVariables, TContext>);
22
23
/**
24
* Execute the mutation with the given variables
25
* @param variables - Variables to pass to the mutation function
26
* @param options - Additional options for this specific mutation execution
27
* @returns Promise resolving to the mutation result
28
*/
29
mutate(variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>): Promise<TData>;
30
31
/**
32
* Get the current result snapshot
33
* @returns Current mutation observer result
34
*/
35
getCurrentResult(): MutationObserverResult<TData, TError, TVariables, TContext>;
36
37
/**
38
* Subscribe to mutation state changes
39
* @param onStoreChange - Callback called when mutation state changes
40
* @returns Unsubscribe function
41
*/
42
subscribe(onStoreChange: (result: MutationObserverResult<TData, TError, TVariables, TContext>) => void): () => void;
43
44
/**
45
* Update mutation observer options
46
* @param options - New options to merge
47
*/
48
setOptions(options: MutationObserverOptions<TData, TError, TVariables, TContext>): void;
49
50
/**
51
* Reset the mutation to its initial state
52
* Clears data, error, and resets status to idle
53
*/
54
reset(): void;
55
56
/**
57
* Destroy the observer and cleanup subscriptions
58
*/
59
destroy(): void;
60
}
61
62
interface MutationObserverOptions<
63
TData = unknown,
64
TError = Error,
65
TVariables = void,
66
TContext = unknown
67
> {
68
/**
69
* Optional mutation key for identification and scoping
70
*/
71
mutationKey?: MutationKey;
72
73
/**
74
* The mutation function that performs the actual mutation
75
* @param variables - Variables passed to the mutation
76
* @returns Promise resolving to the mutation result
77
*/
78
mutationFn?: MutationFunction<TData, TVariables>;
79
80
/**
81
* Function called before the mutation executes
82
* Useful for optimistic updates
83
* @param variables - Variables being passed to mutation
84
* @returns Context value passed to other callbacks
85
*/
86
onMutate?: (variables: TVariables) => Promise<TContext> | TContext;
87
88
/**
89
* Function called if the mutation succeeds
90
* @param data - Data returned by the mutation
91
* @param variables - Variables that were passed to mutation
92
* @param context - Context returned from onMutate
93
*/
94
onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
95
96
/**
97
* Function called if the mutation fails
98
* @param error - Error thrown by the mutation
99
* @param variables - Variables that were passed to mutation
100
* @param context - Context returned from onMutate
101
*/
102
onError?: (error: TError, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
103
104
/**
105
* Function called when the mutation settles (success or error)
106
* @param data - Data returned by mutation (undefined if error)
107
* @param error - Error thrown by mutation (null if success)
108
* @param variables - Variables that were passed to mutation
109
* @param context - Context returned from onMutate
110
*/
111
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
112
113
/**
114
* Number of retry attempts for failed mutations
115
*/
116
retry?: RetryValue<TError>;
117
118
/**
119
* Delay between retry attempts
120
*/
121
retryDelay?: RetryDelayValue<TError>;
122
123
/**
124
* Whether to throw errors or handle them in the result
125
*/
126
throwOnError?: ThrowOnError<TData, TError, TVariables, TContext>;
127
128
/**
129
* Additional metadata for the mutation
130
*/
131
meta?: MutationMeta;
132
133
/**
134
* Network mode for the mutation
135
*/
136
networkMode?: NetworkMode;
137
138
/**
139
* Global mutation ID for scoped mutations
140
* Only one mutation with the same scope can run at a time
141
*/
142
scope?: {
143
id: string;
144
};
145
}
146
147
interface MutationObserverResult<
148
TData = unknown,
149
TError = Error,
150
TVariables = void,
151
TContext = unknown
152
> {
153
/** The data returned by the mutation */
154
data: TData | undefined;
155
156
/** The error thrown by the mutation */
157
error: TError | null;
158
159
/** The variables passed to the mutation */
160
variables: TVariables | undefined;
161
162
/** Whether the mutation is currently idle */
163
isIdle: boolean;
164
165
/** Whether the mutation is currently pending */
166
isPending: boolean;
167
168
/** Whether the mutation completed successfully */
169
isSuccess: boolean;
170
171
/** Whether the mutation failed */
172
isError: boolean;
173
174
/** Whether the mutation is paused */
175
isPaused: boolean;
176
177
/** The current status of the mutation */
178
status: MutationStatus;
179
180
/** Number of times the mutation has failed */
181
failureCount: number;
182
183
/** The reason for the most recent failure */
184
failureReason: TError | null;
185
186
/** Function to execute the mutation */
187
mutate: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => void;
188
189
/** Function to execute the mutation and return a promise */
190
mutateAsync: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => Promise<TData>;
191
192
/** Function to reset the mutation state */
193
reset: () => void;
194
195
/** Context returned from onMutate */
196
context: TContext | undefined;
197
198
/** Timestamp when the mutation was submitted */
199
submittedAt: number;
200
}
201
202
interface MutateOptions<
203
TData = unknown,
204
TError = Error,
205
TVariables = void,
206
TContext = unknown
207
> {
208
onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
209
onError?: (error: TError, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
210
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
211
throwOnError?: ThrowOnError<TData, TError, TVariables, TContext>;
212
}
213
```
214
215
**Usage Examples:**
216
217
```typescript
218
import { QueryClient, MutationObserver } from "@tanstack/query-core";
219
220
const queryClient = new QueryClient();
221
222
// Create mutation observer
223
const mutationObserver = new MutationObserver(queryClient, {
224
mutationFn: async (userData) => {
225
const response = await fetch('/api/users', {
226
method: 'POST',
227
headers: { 'Content-Type': 'application/json' },
228
body: JSON.stringify(userData),
229
});
230
return response.json();
231
},
232
onSuccess: (data, variables) => {
233
console.log('User created:', data);
234
235
// Invalidate users list
236
queryClient.invalidateQueries({ queryKey: ['users'] });
237
},
238
onError: (error, variables) => {
239
console.error('Failed to create user:', error);
240
},
241
});
242
243
// Subscribe to mutation state
244
const unsubscribe = mutationObserver.subscribe((result) => {
245
console.log('Status:', result.status);
246
console.log('Is pending:', result.isPending);
247
console.log('Data:', result.data);
248
console.log('Error:', result.error);
249
250
if (result.isSuccess) {
251
console.log('Mutation succeeded with data:', result.data);
252
}
253
254
if (result.isError) {
255
console.error('Mutation failed with error:', result.error);
256
}
257
});
258
259
// Execute mutation
260
try {
261
const newUser = await mutationObserver.mutate({
262
name: 'John Doe',
263
email: 'john@example.com',
264
});
265
console.log('Created user:', newUser);
266
} catch (error) {
267
console.error('Mutation failed:', error);
268
}
269
270
// Reset mutation state
271
mutationObserver.reset();
272
273
// Cleanup
274
unsubscribe();
275
mutationObserver.destroy();
276
```
277
278
### Optimistic Updates
279
280
Implementing optimistic updates with proper rollback handling.
281
282
```typescript { .api }
283
// Optimistic updates with rollback
284
const updateUserMutation = new MutationObserver(queryClient, {
285
mutationFn: async ({ id, updates }) => {
286
const response = await fetch(`/api/users/${id}`, {
287
method: 'PATCH',
288
headers: { 'Content-Type': 'application/json' },
289
body: JSON.stringify(updates),
290
});
291
if (!response.ok) throw new Error('Update failed');
292
return response.json();
293
},
294
295
// Optimistic update
296
onMutate: async ({ id, updates }) => {
297
// Cancel outgoing refetches so they don't overwrite optimistic update
298
await queryClient.cancelQueries({ queryKey: ['user', id] });
299
300
// Snapshot the previous value
301
const previousUser = queryClient.getQueryData(['user', id]);
302
303
// Optimistically update the cache
304
queryClient.setQueryData(['user', id], (old) => ({
305
...old,
306
...updates,
307
}));
308
309
// Return context with previous value for rollback
310
return { previousUser };
311
},
312
313
// Rollback on error
314
onError: (error, { id }, context) => {
315
// Restore previous value on error
316
if (context?.previousUser) {
317
queryClient.setQueryData(['user', id], context.previousUser);
318
}
319
},
320
321
// Always refetch after success or error
322
onSettled: (data, error, { id }) => {
323
queryClient.invalidateQueries({ queryKey: ['user', id] });
324
},
325
});
326
```
327
328
### Mutation Cache Management
329
330
Direct access to mutation cache for advanced scenarios.
331
332
```typescript { .api }
333
/**
334
* Resume all paused mutations
335
* Useful after network reconnection
336
* @returns Promise that resolves when all mutations are resumed
337
*/
338
resumePausedMutations(): Promise<unknown>;
339
340
/**
341
* Get the mutation cache instance
342
* @returns The mutation cache
343
*/
344
getMutationCache(): MutationCache;
345
346
/**
347
* Check how many mutations are currently running
348
* @param filters - Optional filters to narrow the count
349
* @returns Number of pending mutations
350
*/
351
isMutating(filters?: MutationFilters): number;
352
```
353
354
**Usage Examples:**
355
356
```typescript
357
// Resume paused mutations after reconnection
358
await queryClient.resumePausedMutations();
359
360
// Check if any mutations are running
361
const mutationCount = queryClient.isMutating();
362
if (mutationCount > 0) {
363
console.log(`${mutationCount} mutations currently running`);
364
}
365
366
// Check specific mutations
367
const userMutationCount = queryClient.isMutating({
368
mutationKey: ['user'],
369
});
370
371
// Access mutation cache directly
372
const mutationCache = queryClient.getMutationCache();
373
const allMutations = mutationCache.getAll();
374
console.log('All mutations:', allMutations);
375
```
376
377
### Scoped Mutations
378
379
Managing concurrent mutations with scoping to prevent conflicts.
380
381
```typescript { .api }
382
interface MutationObserverOptions<T> {
383
/**
384
* Scope configuration for limiting concurrent mutations
385
* Only one mutation with the same scope ID can run at once
386
*/
387
scope?: {
388
id: string;
389
};
390
}
391
```
392
393
**Usage Examples:**
394
395
```typescript
396
// Scoped mutations - only one per user at a time
397
const createScopedUserMutation = (userId) => new MutationObserver(queryClient, {
398
mutationFn: async (updates) => {
399
const response = await fetch(`/api/users/${userId}`, {
400
method: 'PATCH',
401
body: JSON.stringify(updates),
402
});
403
return response.json();
404
},
405
scope: {
406
id: `user-${userId}`, // Only one mutation per user
407
},
408
});
409
410
// Global scope - only one mutation of this type at a time
411
const globalMutation = new MutationObserver(queryClient, {
412
mutationFn: async (data) => {
413
// Critical operation that should not run concurrently
414
const response = await fetch('/api/critical-operation', {
415
method: 'POST',
416
body: JSON.stringify(data),
417
});
418
return response.json();
419
},
420
scope: {
421
id: 'critical-operation',
422
},
423
});
424
```
425
426
### Side Effects and Invalidation
427
428
Common patterns for handling side effects after mutations.
429
430
```typescript { .api }
431
// Complex side effects with multiple invalidations
432
const complexMutation = new MutationObserver(queryClient, {
433
mutationFn: async (data) => {
434
const response = await fetch('/api/complex-operation', {
435
method: 'POST',
436
body: JSON.stringify(data),
437
});
438
return response.json();
439
},
440
441
onSuccess: async (data, variables) => {
442
// Invalidate multiple related queries
443
await Promise.all([
444
queryClient.invalidateQueries({ queryKey: ['users'] }),
445
queryClient.invalidateQueries({ queryKey: ['posts', data.userId] }),
446
queryClient.invalidateQueries({ queryKey: ['notifications'] }),
447
]);
448
449
// Update specific cached data
450
queryClient.setQueryData(['user', data.userId], (old) => ({
451
...old,
452
lastActivity: new Date().toISOString(),
453
}));
454
455
// Prefetch related data
456
await queryClient.prefetchQuery({
457
queryKey: ['user-stats', data.userId],
458
queryFn: () => fetch(`/api/users/${data.userId}/stats`).then(r => r.json()),
459
});
460
},
461
462
onError: (error, variables) => {
463
// Handle specific error types
464
if (error.status === 409) {
465
// Conflict - refresh conflicting data
466
queryClient.invalidateQueries({ queryKey: ['conflicts'] });
467
} else if (error.status >= 500) {
468
// Server error - maybe retry later
469
console.error('Server error, consider retrying:', error);
470
}
471
},
472
});
473
```
474
475
## Core Types
476
477
```typescript { .api }
478
type MutationKey = ReadonlyArray<unknown>;
479
480
type MutationFunction<TData = unknown, TVariables = void> = (variables: TVariables) => Promise<TData>;
481
482
type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
483
484
type RetryValue<TError> = boolean | number | ((failureCount: number, error: TError) => boolean);
485
486
type RetryDelayValue<TError> = number | ((failureCount: number, error: TError, mutation: Mutation) => number);
487
488
type ThrowOnError<TData, TError, TVariables, TContext> =
489
| boolean
490
| ((error: TError, variables: TVariables, context: TContext | undefined) => boolean);
491
492
type NetworkMode = 'online' | 'always' | 'offlineFirst';
493
494
interface MutationMeta extends Record<string, unknown> {}
495
496
interface MutationFilters {
497
mutationKey?: MutationKey;
498
exact?: boolean;
499
predicate?: (mutation: Mutation) => boolean;
500
status?: MutationStatus;
501
}
502
```