or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdcore-query-hooks.mdindex.mdmutations.mdproviders-context.mdsuspense-integration.mdutilities.md

mutations.mddocs/

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.