or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

cache-management.mdcore-data-fetching.mdglobal-configuration.mdimmutable-data.mdindex.mdinfinite-loading.mdmutations.mdsubscriptions.md

mutations.mddocs/

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

```