0
# Remote Mutations
1
2
The `useSWRMutation` hook handles remote mutations (POST, PUT, DELETE, PATCH) with optimistic updates, error handling, and cache management.
3
4
## Capabilities
5
6
### useSWRMutation Hook
7
8
Hook for handling remote mutations with optimistic updates and rollback support.
9
10
```typescript { .api }
11
/**
12
* Hook for remote mutations with optimistic updates and rollback support
13
* @param key - Unique identifier for the mutation
14
* @param fetcher - Function that performs the mutation
15
* @param config - Configuration options for the mutation
16
* @returns SWRMutationResponse with trigger function and mutation state
17
*/
18
function useSWRMutation<Data = any, Error = any, ExtraArg = never>(
19
key: Key,
20
fetcher: MutationFetcher<Data, Key, ExtraArg>,
21
config?: SWRMutationConfiguration<Data, Error, Key, ExtraArg>
22
): SWRMutationResponse<Data, Error, Key, ExtraArg>;
23
```
24
25
**Usage Examples:**
26
27
```typescript
28
import useSWRMutation from "swr/mutation";
29
30
// Basic mutation
31
const { trigger, isMutating, data, error } = useSWRMutation(
32
"/api/user",
33
async (url, { arg }: { arg: { name: string } }) => {
34
const response = await fetch(url, {
35
method: "POST",
36
headers: { "Content-Type": "application/json" },
37
body: JSON.stringify(arg)
38
});
39
return response.json();
40
}
41
);
42
43
// Trigger the mutation
44
const handleSubmit = async (formData: { name: string }) => {
45
try {
46
const result = await trigger(formData);
47
console.log("User created:", result);
48
} catch (error) {
49
console.error("Failed to create user:", error);
50
}
51
};
52
53
// Mutation with optimistic updates
54
const { trigger } = useSWRMutation(
55
"/api/user/123",
56
updateUserFetcher,
57
{
58
optimisticData: (currentData) => ({ ...currentData, updating: true }),
59
rollbackOnError: true,
60
populateCache: true,
61
revalidate: false,
62
}
63
);
64
```
65
66
### SWR Mutation Response
67
68
The return value from `useSWRMutation` with mutation control and state.
69
70
```typescript { .api }
71
interface SWRMutationResponse<Data, Error, Key, ExtraArg> {
72
/** The data returned by the mutation (undefined if not triggered or error) */
73
data: Data | undefined;
74
/** The error thrown by the mutation (undefined if no error) */
75
error: Error | undefined;
76
/** Function to trigger the mutation */
77
trigger: TriggerFunction<Data, Error, Key, ExtraArg>;
78
/** Function to reset the mutation state */
79
reset: () => void;
80
/** True when the mutation is in progress */
81
isMutating: boolean;
82
}
83
84
// Trigger function types based on ExtraArg requirements
85
type TriggerFunction<Data, Error, Key, ExtraArg> =
86
ExtraArg extends never
87
? () => Promise<Data | undefined>
88
: ExtraArg extends undefined
89
? (arg?: ExtraArg, options?: SWRMutationConfiguration<Data, Error, Key, ExtraArg>) => Promise<Data | undefined>
90
: (arg: ExtraArg, options?: SWRMutationConfiguration<Data, Error, Key, ExtraArg>) => Promise<Data | undefined>;
91
```
92
93
### Mutation Fetcher
94
95
Function that performs the actual mutation operation.
96
97
```typescript { .api }
98
type MutationFetcher<Data, SWRKey, ExtraArg> = (
99
key: SWRKey,
100
options: { arg: ExtraArg }
101
) => Data | Promise<Data>;
102
```
103
104
**Mutation Fetcher Examples:**
105
106
```typescript
107
// Simple POST request
108
const createUser = async (url: string, { arg }: { arg: UserData }) => {
109
const response = await fetch(url, {
110
method: "POST",
111
headers: { "Content-Type": "application/json" },
112
body: JSON.stringify(arg)
113
});
114
115
if (!response.ok) {
116
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
117
}
118
119
return response.json();
120
};
121
122
// PUT request with authentication
123
const updateUser = async (url: string, { arg }: { arg: Partial<User> }) => {
124
const response = await fetch(url, {
125
method: "PUT",
126
headers: {
127
"Content-Type": "application/json",
128
"Authorization": `Bearer ${getToken()}`
129
},
130
body: JSON.stringify(arg)
131
});
132
133
return response.json();
134
};
135
136
// DELETE request
137
const deleteUser = async (url: string) => {
138
await fetch(url, { method: "DELETE" });
139
return { deleted: true };
140
};
141
142
// File upload
143
const uploadFile = async (url: string, { arg }: { arg: File }) => {
144
const formData = new FormData();
145
formData.append("file", arg);
146
147
const response = await fetch(url, {
148
method: "POST",
149
body: formData
150
});
151
152
return response.json();
153
};
154
155
// GraphQL mutation
156
const graphqlMutation = async (url: string, { arg }: { arg: { query: string, variables: any } }) => {
157
const response = await fetch(url, {
158
method: "POST",
159
headers: { "Content-Type": "application/json" },
160
body: JSON.stringify(arg)
161
});
162
163
const result = await response.json();
164
165
if (result.errors) {
166
throw new Error(result.errors[0].message);
167
}
168
169
return result.data;
170
};
171
```
172
173
### Configuration Options
174
175
Configuration options for customizing mutation behavior.
176
177
```typescript { .api }
178
interface SWRMutationConfiguration<Data = any, Error = any, SWRMutationKey = any, ExtraArg = any> {
179
/** Whether to revalidate related SWR data after mutation (default: true) */
180
revalidate?: boolean;
181
/** Whether to update cache with mutation result (default: true) */
182
populateCache?: boolean | ((result: Data, currentData: Data | undefined) => Data);
183
/** Data to show optimistically during mutation */
184
optimisticData?: Data | ((currentData: Data | undefined) => Data);
185
/** Whether to rollback optimistic data on error (default: true) */
186
rollbackOnError?: boolean | ((error: any) => boolean);
187
/** Mutation fetcher function */
188
fetcher?: MutationFetcher<Data, SWRMutationKey, ExtraArg>;
189
/** Success callback */
190
onSuccess?: (data: Data, key: SWRMutationKey, config: SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg>) => void;
191
/** Error callback */
192
onError?: (err: Error, key: SWRMutationKey, config: SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg>) => void;
193
}
194
```
195
196
**Configuration Examples:**
197
198
```typescript
199
// Optimistic updates with rollback
200
const { trigger } = useSWRMutation("/api/like", likeFetcher, {
201
optimisticData: (current) => ({ ...current, liked: true, likes: current.likes + 1 }),
202
rollbackOnError: true,
203
populateCache: false, // Don't update cache, let revalidation handle it
204
revalidate: true
205
});
206
207
// Custom cache population
208
const { trigger } = useSWRMutation("/api/user", updateUser, {
209
populateCache: (result, currentData) => ({
210
...currentData,
211
...result,
212
lastUpdated: Date.now()
213
}),
214
revalidate: false // Skip revalidation since we manually populated cache
215
});
216
217
// Conditional rollback
218
const { trigger } = useSWRMutation("/api/data", mutationFetcher, {
219
rollbackOnError: (error) => error.status >= 500, // Only rollback on server errors
220
onError: (error, key) => {
221
if (error.status === 400) {
222
showValidationErrors(error.validation);
223
} else {
224
showGenericError();
225
}
226
}
227
});
228
229
// Success handling
230
const { trigger } = useSWRMutation("/api/user", createUser, {
231
onSuccess: (data, key) => {
232
showNotification(`User ${data.name} created successfully!`);
233
// Invalidate related data
234
mutate("/api/users"); // Refresh users list
235
}
236
});
237
```
238
239
### Advanced Mutation Patterns
240
241
Common patterns for complex mutation scenarios.
242
243
**Form Submission:**
244
245
```typescript
246
function UserForm() {
247
const [formData, setFormData] = useState({ name: "", email: "" });
248
249
const { trigger, isMutating, error } = useSWRMutation(
250
"/api/users",
251
async (url, { arg }: { arg: typeof formData }) => {
252
const response = await fetch(url, {
253
method: "POST",
254
headers: { "Content-Type": "application/json" },
255
body: JSON.stringify(arg)
256
});
257
258
if (!response.ok) {
259
const errorData = await response.json();
260
throw new Error(errorData.message);
261
}
262
263
return response.json();
264
},
265
{
266
onSuccess: () => {
267
setFormData({ name: "", email: "" }); // Reset form
268
showNotification("User created successfully!");
269
}
270
}
271
);
272
273
const handleSubmit = async (e: React.FormEvent) => {
274
e.preventDefault();
275
await trigger(formData);
276
};
277
278
return (
279
<form onSubmit={handleSubmit}>
280
<input
281
value={formData.name}
282
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
283
disabled={isMutating}
284
/>
285
<input
286
value={formData.email}
287
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
288
disabled={isMutating}
289
/>
290
<button type="submit" disabled={isMutating}>
291
{isMutating ? "Creating..." : "Create User"}
292
</button>
293
{error && <div>Error: {error.message}</div>}
294
</form>
295
);
296
}
297
```
298
299
**Optimistic Updates:**
300
301
```typescript
302
function LikeButton({ postId, initialLikes, initialLiked }: LikeButtonProps) {
303
const { data: post } = useSWR(`/api/posts/${postId}`, fetcher, {
304
fallbackData: { likes: initialLikes, liked: initialLiked }
305
});
306
307
const { trigger } = useSWRMutation(
308
`/api/posts/${postId}/like`,
309
async (url) => {
310
const response = await fetch(url, { method: "POST" });
311
return response.json();
312
},
313
{
314
optimisticData: (current) => ({
315
...current,
316
liked: !current.liked,
317
likes: current.liked ? current.likes - 1 : current.likes + 1
318
}),
319
rollbackOnError: true,
320
revalidate: false // Rely on optimistic update
321
}
322
);
323
324
const handleLike = () => trigger();
325
326
return (
327
<button onClick={handleLike}>
328
{post.liked ? "❤️" : "🤍"} {post.likes}
329
</button>
330
);
331
}
332
```
333
334
**Batch Operations:**
335
336
```typescript
337
function BulkActions({ selectedItems }: { selectedItems: string[] }) {
338
const { trigger: bulkDelete, isMutating } = useSWRMutation(
339
"/api/items/bulk-delete",
340
async (url, { arg }: { arg: string[] }) => {
341
const response = await fetch(url, {
342
method: "DELETE",
343
headers: { "Content-Type": "application/json" },
344
body: JSON.stringify({ ids: arg })
345
});
346
return response.json();
347
},
348
{
349
onSuccess: (result) => {
350
showNotification(`${result.deletedCount} items deleted`);
351
// Revalidate lists
352
mutate(key => typeof key === "string" && key.startsWith("/api/items"));
353
}
354
}
355
);
356
357
const handleBulkDelete = async () => {
358
if (confirm(`Delete ${selectedItems.length} items?`)) {
359
await trigger(selectedItems);
360
}
361
};
362
363
return (
364
<button
365
onClick={handleBulkDelete}
366
disabled={isMutating || selectedItems.length === 0}
367
>
368
{isMutating ? "Deleting..." : `Delete ${selectedItems.length} items`}
369
</button>
370
);
371
}
372
```
373
374
**File Upload with Progress:**
375
376
```typescript
377
function FileUpload() {
378
const [uploadProgress, setUploadProgress] = useState(0);
379
380
const { trigger, isMutating, data, error } = useSWRMutation(
381
"/api/upload",
382
async (url, { arg }: { arg: File }) => {
383
return new Promise((resolve, reject) => {
384
const formData = new FormData();
385
formData.append("file", arg);
386
387
const xhr = new XMLHttpRequest();
388
389
xhr.upload.addEventListener("progress", (e) => {
390
if (e.lengthComputable) {
391
setUploadProgress(Math.round((e.loaded / e.total) * 100));
392
}
393
});
394
395
xhr.onload = () => {
396
if (xhr.status === 200) {
397
resolve(JSON.parse(xhr.responseText));
398
} else {
399
reject(new Error(`Upload failed: ${xhr.statusText}`));
400
}
401
};
402
403
xhr.onerror = () => reject(new Error("Upload failed"));
404
405
xhr.open("POST", url);
406
xhr.send(formData);
407
});
408
},
409
{
410
onSuccess: () => {
411
setUploadProgress(0);
412
showNotification("File uploaded successfully!");
413
}
414
}
415
);
416
417
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
418
const file = e.target.files?.[0];
419
if (file) {
420
trigger(file);
421
}
422
};
423
424
return (
425
<div>
426
<input
427
type="file"
428
onChange={handleFileSelect}
429
disabled={isMutating}
430
/>
431
432
{isMutating && (
433
<div>
434
<div>Uploading... {uploadProgress}%</div>
435
<progress value={uploadProgress} max={100} />
436
</div>
437
)}
438
439
{data && <div>Uploaded: {data.filename}</div>}
440
{error && <div>Error: {error.message}</div>}
441
</div>
442
);
443
}
444
```