or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

browser-integration.mdcache-management.mdclient-management.mdhydration.mdindex.mdinfinite-queries.mdmutations.mdquery-observers.mdquery-operations.mdutilities.md

mutations.mddocs/

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

```