0
# Mutations
1
2
Hooks for data modification with optimistic updates, error handling, and automatic query invalidation.
3
4
## useMutation
5
6
**Hook for creating, updating, or deleting data with optimistic updates and rollback capabilities**
7
8
```typescript { .api }
9
function useMutation<
10
TData = unknown,
11
TError = DefaultError,
12
TVariables = void,
13
TContext = unknown,
14
>(
15
options: UseMutationOptions<TData, TError, TVariables, TContext>,
16
queryClient?: QueryClient,
17
): UseMutationResult<TData, TError, TVariables, TContext>
18
```
19
20
### Options
21
22
```typescript { .api }
23
interface UseMutationOptions<
24
TData = unknown,
25
TError = DefaultError,
26
TVariables = void,
27
TContext = unknown,
28
> {
29
mutationFn?: MutationFunction<TData, TVariables>
30
mutationKey?: MutationKey
31
onMutate?: (variables: TVariables) => Promise<TContext> | TContext | void
32
onError?: (
33
error: TError,
34
variables: TVariables,
35
context: TContext | undefined,
36
) => Promise<unknown> | unknown
37
onSuccess?: (
38
data: TData,
39
variables: TVariables,
40
context: TContext | undefined,
41
) => Promise<unknown> | unknown
42
onSettled?: (
43
data: TData | undefined,
44
error: TError | null,
45
variables: TVariables,
46
context: TContext | undefined,
47
) => Promise<unknown> | unknown
48
retry?: boolean | number | ((failureCount: number, error: TError) => boolean)
49
retryDelay?: number | ((retryAttempt: number, error: TError) => number)
50
networkMode?: 'online' | 'always' | 'offlineFirst'
51
gcTime?: number
52
meta?: Record<string, unknown>
53
}
54
```
55
56
### Result
57
58
```typescript { .api }
59
interface UseMutationResult<
60
TData = unknown,
61
TError = DefaultError,
62
TVariables = unknown,
63
TContext = unknown,
64
> {
65
data: TData | undefined
66
error: TError | null
67
isError: boolean
68
isIdle: boolean
69
isPending: boolean
70
isPaused: boolean
71
isSuccess: boolean
72
failureCount: number
73
failureReason: TError | null
74
mutate: (
75
variables: TVariables,
76
options?: {
77
onSuccess?: (data: TData, variables: TVariables, context: TContext) => void
78
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void
79
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void
80
}
81
) => void
82
mutateAsync: (
83
variables: TVariables,
84
options?: {
85
onSuccess?: (data: TData, variables: TVariables, context: TContext) => void
86
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void
87
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void
88
}
89
) => Promise<TData>
90
reset: () => void
91
status: 'idle' | 'pending' | 'error' | 'success'
92
submittedAt: number
93
variables: TVariables | undefined
94
}
95
```
96
97
### Basic Usage
98
99
```typescript { .api }
100
import { useMutation, useQueryClient } from '@tanstack/react-query'
101
102
interface CreatePostRequest {
103
title: string
104
content: string
105
}
106
107
interface Post {
108
id: number
109
title: string
110
content: string
111
createdAt: string
112
}
113
114
function CreatePostForm() {
115
const queryClient = useQueryClient()
116
117
const mutation = useMutation<Post, Error, CreatePostRequest>({
118
mutationFn: async (newPost) => {
119
const response = await fetch('/api/posts', {
120
method: 'POST',
121
headers: { 'Content-Type': 'application/json' },
122
body: JSON.stringify(newPost)
123
})
124
125
if (!response.ok) {
126
throw new Error('Failed to create post')
127
}
128
129
return response.json()
130
},
131
onSuccess: (data) => {
132
// Invalidate and refetch posts
133
queryClient.invalidateQueries({ queryKey: ['posts'] })
134
// Or add the new post to existing cache
135
queryClient.setQueryData(['posts'], (oldPosts: Post[]) => [...oldPosts, data])
136
},
137
onError: (error) => {
138
console.error('Error creating post:', error.message)
139
}
140
})
141
142
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
143
event.preventDefault()
144
const formData = new FormData(event.currentTarget)
145
146
mutation.mutate({
147
title: formData.get('title') as string,
148
content: formData.get('content') as string
149
})
150
}
151
152
return (
153
<form onSubmit={handleSubmit}>
154
<input name="title" placeholder="Post title" required />
155
<textarea name="content" placeholder="Post content" required />
156
<button type="submit" disabled={mutation.isPending}>
157
{mutation.isPending ? 'Creating...' : 'Create Post'}
158
</button>
159
160
{mutation.isError && (
161
<div style={{ color: 'red' }}>
162
Error: {mutation.error?.message}
163
</div>
164
)}
165
166
{mutation.isSuccess && (
167
<div style={{ color: 'green' }}>
168
Post created successfully!
169
</div>
170
)}
171
</form>
172
)
173
}
174
```
175
176
### Optimistic Updates
177
178
```typescript { .api }
179
interface UpdatePostRequest {
180
id: number
181
title: string
182
content: string
183
}
184
185
function useUpdatePost() {
186
const queryClient = useQueryClient()
187
188
return useMutation<Post, Error, UpdatePostRequest, { previousPost?: Post }>({
189
mutationFn: async (updatedPost) => {
190
const response = await fetch(`/api/posts/${updatedPost.id}`, {
191
method: 'PUT',
192
headers: { 'Content-Type': 'application/json' },
193
body: JSON.stringify(updatedPost)
194
})
195
return response.json()
196
},
197
onMutate: async (updatedPost) => {
198
// Cancel any outgoing refetches
199
await queryClient.cancelQueries({ queryKey: ['post', updatedPost.id] })
200
201
// Snapshot the previous value
202
const previousPost = queryClient.getQueryData<Post>(['post', updatedPost.id])
203
204
// Optimistically update to the new value
205
queryClient.setQueryData(['post', updatedPost.id], updatedPost)
206
207
// Return a context object with the snapshotted value
208
return { previousPost }
209
},
210
onError: (err, updatedPost, context) => {
211
// If the mutation fails, use the context to roll back
212
if (context?.previousPost) {
213
queryClient.setQueryData(['post', updatedPost.id], context.previousPost)
214
}
215
},
216
onSettled: (data, error, updatedPost) => {
217
// Always refetch after error or success
218
queryClient.invalidateQueries({ queryKey: ['post', updatedPost.id] })
219
}
220
})
221
}
222
223
function EditPostForm({ post }: { post: Post }) {
224
const updateMutation = useUpdatePost()
225
226
const handleSubmit = (event: React.FormEvent) => {
227
event.preventDefault()
228
const formData = new FormData(event.currentTarget)
229
230
updateMutation.mutate({
231
id: post.id,
232
title: formData.get('title') as string,
233
content: formData.get('content') as string
234
})
235
}
236
237
return (
238
<form onSubmit={handleSubmit}>
239
<input name="title" defaultValue={post.title} />
240
<textarea name="content" defaultValue={post.content} />
241
<button type="submit" disabled={updateMutation.isPending}>
242
Update Post
243
</button>
244
</form>
245
)
246
}
247
```
248
249
### Async/Await Pattern
250
251
```typescript { .api }
252
function useCreatePost() {
253
const queryClient = useQueryClient()
254
255
return useMutation<Post, Error, CreatePostRequest>({
256
mutationFn: async (newPost) => {
257
const response = await fetch('/api/posts', {
258
method: 'POST',
259
headers: { 'Content-Type': 'application/json' },
260
body: JSON.stringify(newPost)
261
})
262
return response.json()
263
},
264
onSuccess: () => {
265
queryClient.invalidateQueries({ queryKey: ['posts'] })
266
}
267
})
268
}
269
270
function CreatePostModal() {
271
const [isOpen, setIsOpen] = useState(false)
272
const createMutation = useCreatePost()
273
274
const handleCreate = async (postData: CreatePostRequest) => {
275
try {
276
const newPost = await createMutation.mutateAsync(postData)
277
console.log('Created post:', newPost)
278
setIsOpen(false) // Close modal on success
279
} catch (error) {
280
console.error('Failed to create post:', error)
281
// Error handling - modal stays open
282
}
283
}
284
285
return (
286
<div>
287
<button onClick={() => setIsOpen(true)}>Create Post</button>
288
{isOpen && (
289
<Modal>
290
<CreatePostForm
291
onSubmit={handleCreate}
292
isLoading={createMutation.isPending}
293
/>
294
</Modal>
295
)}
296
</div>
297
)
298
}
299
```
300
301
### Global Mutation Handling
302
303
```typescript { .api }
304
const queryClient = new QueryClient({
305
mutationCache: new MutationCache({
306
onError: (error, variables, context, mutation) => {
307
// Global error handling
308
console.error(`Mutation failed:`, error)
309
310
// Show toast notification
311
toast.error(`Operation failed: ${error.message}`)
312
},
313
onSuccess: (data, variables, context, mutation) => {
314
// Global success handling
315
if (mutation.options.meta?.successMessage) {
316
toast.success(mutation.options.meta.successMessage)
317
}
318
}
319
})
320
})
321
322
// Usage with meta
323
const mutation = useMutation({
324
mutationFn: createPost,
325
meta: {
326
successMessage: 'Post created successfully!'
327
}
328
})
329
```
330
331
## useMutationState
332
333
**Hook for accessing mutation state across components**
334
335
```typescript { .api }
336
function useMutationState<TResult = MutationState>(
337
options?: {
338
filters?: MutationFilters
339
select?: (mutation: Mutation) => TResult
340
},
341
queryClient?: QueryClient,
342
): Array<TResult>
343
```
344
345
### Basic Usage
346
347
```typescript { .api }
348
// Monitor all pending mutations
349
function GlobalLoadingIndicator() {
350
const pendingMutations = useMutationState({
351
filters: { status: 'pending' }
352
})
353
354
if (pendingMutations.length === 0) return null
355
356
return (
357
<div className="loading-indicator">
358
{pendingMutations.length} operation{pendingMutations.length > 1 ? 's' : ''} in progress...
359
</div>
360
)
361
}
362
363
// Monitor specific mutation types
364
function PostOperations() {
365
const postMutations = useMutationState({
366
filters: { mutationKey: ['posts'] },
367
select: (mutation) => ({
368
status: mutation.state.status,
369
variables: mutation.state.variables,
370
error: mutation.state.error,
371
submittedAt: mutation.state.submittedAt
372
})
373
})
374
375
return (
376
<div>
377
<h3>Post Operations</h3>
378
{postMutations.map((mutation, index) => (
379
<div key={index}>
380
Status: {mutation.status}
381
{mutation.error && <span> - Error: {mutation.error.message}</span>}
382
</div>
383
))}
384
</div>
385
)
386
}
387
```
388
389
### Advanced State Selection
390
391
```typescript { .api }
392
function MutationHistory() {
393
const recentMutations = useMutationState({
394
select: (mutation) => ({
395
id: mutation.mutationId,
396
key: mutation.options.mutationKey?.[0] || 'unknown',
397
status: mutation.state.status,
398
submittedAt: mutation.state.submittedAt,
399
variables: mutation.state.variables,
400
error: mutation.state.error?.message
401
})
402
})
403
404
const sortedMutations = recentMutations
405
.sort((a, b) => b.submittedAt - a.submittedAt)
406
.slice(0, 10) // Last 10 mutations
407
408
return (
409
<div>
410
<h3>Recent Operations</h3>
411
{sortedMutations.map((mutation) => (
412
<div key={mutation.id} className={`mutation-${mutation.status}`}>
413
<strong>{mutation.key}</strong> - {mutation.status}
414
<small>{new Date(mutation.submittedAt).toLocaleTimeString()}</small>
415
{mutation.error && <div className="error">{mutation.error}</div>}
416
</div>
417
))}
418
</div>
419
)
420
}
421
```
422
423
## useIsMutating
424
425
**Hook for tracking the number of mutations currently in a pending state**
426
427
```typescript { .api }
428
function useIsMutating(
429
filters?: MutationFilters,
430
queryClient?: QueryClient,
431
): number
432
```
433
434
### Basic Usage
435
436
```typescript { .api }
437
function App() {
438
const isMutating = useIsMutating()
439
440
return (
441
<div>
442
{isMutating > 0 && (
443
<div className="global-loading-bar">
444
Saving changes... ({isMutating} operations)
445
</div>
446
)}
447
<Router>
448
{/* App content */}
449
</Router>
450
</div>
451
)
452
}
453
454
// Track specific mutation types
455
function PostsSection() {
456
const isPostMutating = useIsMutating({ mutationKey: ['posts'] })
457
458
return (
459
<div>
460
<h2>Posts {isPostMutating > 0 && '(Saving...)'}</h2>
461
<PostsList />
462
</div>
463
)
464
}
465
```
466
467
### With Filters
468
469
```typescript { .api }
470
function UserDashboard({ userId }: { userId: number }) {
471
// Track mutations for this specific user
472
const userMutationsCount = useIsMutating({
473
mutationKey: ['user', userId]
474
})
475
476
// Track all create operations
477
const createMutationsCount = useIsMutating({
478
predicate: (mutation) =>
479
mutation.options.mutationKey?.[1] === 'create'
480
})
481
482
return (
483
<div>
484
<h1>User Dashboard</h1>
485
{userMutationsCount > 0 && (
486
<div>Updating user data...</div>
487
)}
488
{createMutationsCount > 0 && (
489
<div>Creating {createMutationsCount} new items...</div>
490
)}
491
{/* Dashboard content */}
492
</div>
493
)
494
}
495
```
496
497
## Mutation Patterns
498
499
### Sequential Mutations
500
501
```typescript { .api }
502
function useCreateUserWithProfile() {
503
const queryClient = useQueryClient()
504
505
const createUser = useMutation({
506
mutationFn: (userData: CreateUserRequest) =>
507
fetch('/api/users', {
508
method: 'POST',
509
body: JSON.stringify(userData)
510
}).then(res => res.json())
511
})
512
513
const createProfile = useMutation({
514
mutationFn: ({ userId, profileData }: { userId: number, profileData: any }) =>
515
fetch(`/api/users/${userId}/profile`, {
516
method: 'POST',
517
body: JSON.stringify(profileData)
518
}).then(res => res.json())
519
})
520
521
const createUserWithProfile = async (userData: CreateUserRequest, profileData: any) => {
522
try {
523
const user = await createUser.mutateAsync(userData)
524
const profile = await createProfile.mutateAsync({
525
userId: user.id,
526
profileData
527
})
528
529
queryClient.invalidateQueries({ queryKey: ['users'] })
530
return { user, profile }
531
} catch (error) {
532
throw error
533
}
534
}
535
536
return {
537
createUserWithProfile,
538
isLoading: createUser.isPending || createProfile.isPending,
539
error: createUser.error || createProfile.error
540
}
541
}
542
```
543
544
### Dependent Mutations
545
546
```typescript { .api }
547
function usePublishPost() {
548
const queryClient = useQueryClient()
549
550
return useMutation({
551
mutationFn: async ({ postId }: { postId: number }) => {
552
// First validate the post
553
const validation = await fetch(`/api/posts/${postId}/validate`, {
554
method: 'POST'
555
}).then(res => res.json())
556
557
if (!validation.isValid) {
558
throw new Error(validation.errors.join(', '))
559
}
560
561
// Then publish
562
return fetch(`/api/posts/${postId}/publish`, {
563
method: 'POST'
564
}).then(res => res.json())
565
},
566
onSuccess: (data, variables) => {
567
// Update the post in cache
568
queryClient.setQueryData(['post', variables.postId], data)
569
// Invalidate posts list
570
queryClient.invalidateQueries({ queryKey: ['posts'] })
571
}
572
})
573
}
574
```
575
576
### Batch Operations
577
578
```typescript { .api }
579
function useBatchDeletePosts() {
580
const queryClient = useQueryClient()
581
582
return useMutation({
583
mutationFn: async (postIds: number[]) => {
584
// Delete posts in batches of 10
585
const batches = []
586
for (let i = 0; i < postIds.length; i += 10) {
587
const batch = postIds.slice(i, i + 10)
588
batches.push(
589
fetch('/api/posts/batch-delete', {
590
method: 'POST',
591
headers: { 'Content-Type': 'application/json' },
592
body: JSON.stringify({ ids: batch })
593
}).then(res => res.json())
594
)
595
}
596
597
return Promise.all(batches)
598
},
599
onSuccess: () => {
600
queryClient.invalidateQueries({ queryKey: ['posts'] })
601
}
602
})
603
}
604
605
function PostsManager() {
606
const [selectedPosts, setSelectedPosts] = useState<number[]>([])
607
const batchDelete = useBatchDeletePosts()
608
609
const handleBatchDelete = () => {
610
batchDelete.mutate(selectedPosts, {
611
onSuccess: () => {
612
setSelectedPosts([])
613
}
614
})
615
}
616
617
return (
618
<div>
619
{selectedPosts.length > 0 && (
620
<button
621
onClick={handleBatchDelete}
622
disabled={batchDelete.isPending}
623
>
624
Delete {selectedPosts.length} posts
625
</button>
626
)}
627
{/* Posts list with selection */}
628
</div>
629
)
630
}
631
```
632
633
### Error Recovery
634
635
```typescript { .api }
636
function useCreatePostWithRetry() {
637
return useMutation({
638
mutationFn: createPost,
639
retry: (failureCount, error) => {
640
// Retry network errors up to 3 times
641
if (error.name === 'NetworkError' && failureCount < 3) {
642
return true
643
}
644
// Don't retry validation errors
645
if (error.status === 400) {
646
return false
647
}
648
return failureCount < 2
649
},
650
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
651
})
652
}
653
```
654
655
Mutations in React Query provide powerful data modification capabilities with built-in optimistic updates, error handling, and automatic cache management, making it easy to build responsive and reliable user interfaces.