or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mdinfinite-queries.mdmulti-query-operations.mdmutation-management.mdoptions-helpers.mdprovider-setup.mdquery-management.mdstatus-monitoring.md

mutation-management.mddocs/

0

# Mutation Management

1

2

Mutation functionality for server-side effects with optimistic updates, error handling, and automatic query invalidation using Angular signals.

3

4

## Capabilities

5

6

### Inject Mutation

7

8

Creates a mutation that can be executed to perform server-side effects like creating, updating, or deleting data.

9

10

```typescript { .api }

11

/**

12

* Injects a mutation: an imperative function that can be invoked which typically performs server side effects.

13

* Unlike queries, mutations are not run automatically.

14

* @param injectMutationFn - A function that returns mutation options

15

* @param options - Additional configuration including custom injector

16

* @returns The mutation result with signals and mutate functions

17

*/

18

function injectMutation<TData, TError, TVariables, TContext>(

19

injectMutationFn: () => CreateMutationOptions<TData, TError, TVariables, TContext>,

20

options?: InjectMutationOptions

21

): CreateMutationResult<TData, TError, TVariables, TContext>;

22

```

23

24

**Usage Examples:**

25

26

```typescript

27

import { injectMutation, QueryClient } from "@tanstack/angular-query-experimental";

28

import { Component, inject } from "@angular/core";

29

import { HttpClient } from "@angular/common/http";

30

31

@Component({

32

selector: 'app-user-form',

33

template: `

34

<form (ngSubmit)="handleSubmit()">

35

<input [(ngModel)]="name" placeholder="Name" />

36

<input [(ngModel)]="email" placeholder="Email" />

37

<button

38

type="submit"

39

[disabled]="createUserMutation.isPending()"

40

>

41

{{ createUserMutation.isPending() ? 'Creating...' : 'Create User' }}

42

</button>

43

</form>

44

45

<div *ngIf="createUserMutation.isError()">

46

Error: {{ createUserMutation.error()?.message }}

47

</div>

48

49

<div *ngIf="createUserMutation.isSuccess()">

50

User created: {{ createUserMutation.data()?.name }}

51

</div>

52

`

53

})

54

export class UserFormComponent {

55

#http = inject(HttpClient);

56

#queryClient = inject(QueryClient);

57

58

name = '';

59

email = '';

60

61

// Basic mutation

62

createUserMutation = injectMutation(() => ({

63

mutationFn: (userData: CreateUserRequest) =>

64

this.#http.post<User>('/api/users', userData),

65

onSuccess: (newUser) => {

66

// Invalidate and refetch users list

67

this.#queryClient.invalidateQueries({ queryKey: ['users'] });

68

// Optionally set data directly

69

this.#queryClient.setQueryData(['user', newUser.id], newUser);

70

},

71

onError: (error) => {

72

console.error('Failed to create user:', error);

73

}

74

}));

75

76

// Mutation with optimistic updates

77

updateUserMutation = injectMutation(() => ({

78

mutationFn: (data: { id: number; updates: Partial<User> }) =>

79

this.#http.patch<User>(`/api/users/${data.id}`, data.updates),

80

onMutate: async (variables) => {

81

// Cancel outgoing refetches

82

await this.#queryClient.cancelQueries({ queryKey: ['user', variables.id] });

83

84

// Snapshot previous value

85

const previousUser = this.#queryClient.getQueryData<User>(['user', variables.id]);

86

87

// Optimistically update

88

this.#queryClient.setQueryData(['user', variables.id], (old: User) => ({

89

...old,

90

...variables.updates

91

}));

92

93

return { previousUser };

94

},

95

onError: (error, variables, context) => {

96

// Rollback on error

97

if (context?.previousUser) {

98

this.#queryClient.setQueryData(['user', variables.id], context.previousUser);

99

}

100

},

101

onSettled: (data, error, variables) => {

102

// Always refetch after error or success

103

this.#queryClient.invalidateQueries({ queryKey: ['user', variables.id] });

104

}

105

}));

106

107

handleSubmit() {

108

this.createUserMutation.mutate({

109

name: this.name,

110

email: this.email

111

});

112

}

113

}

114

115

interface User {

116

id: number;

117

name: string;

118

email: string;

119

}

120

121

interface CreateUserRequest {

122

name: string;

123

email: string;

124

}

125

```

126

127

### Mutation Options Interface

128

129

Comprehensive options for configuring mutation behavior.

130

131

```typescript { .api }

132

interface CreateMutationOptions<TData, TError, TVariables, TContext> {

133

/** Function that performs the mutation and returns a promise */

134

mutationFn: MutationFunction<TData, TVariables>;

135

/** Unique key for the mutation (optional) */

136

mutationKey?: MutationKey;

137

/** Called before mutation function is fired */

138

onMutate?: (variables: TVariables) => Promise<TContext> | TContext;

139

/** Called on successful mutation */

140

onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<void> | void;

141

/** Called on mutation error */

142

onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<void> | void;

143

/** Called after mutation completes (success or error) */

144

onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<void> | void;

145

/** Number of retry attempts on failure */

146

retry?: boolean | number | ((failureCount: number, error: TError) => boolean);

147

/** Delay function for retry attempts */

148

retryDelay?: number | ((retryAttempt: number, error: TError) => number);

149

/** Whether to throw errors instead of setting error state */

150

throwOnError?: boolean | ((error: TError) => boolean);

151

/** Time in milliseconds after which unused mutation data is garbage collected */

152

gcTime?: number;

153

/** Custom meta information */

154

meta?: Record<string, unknown>;

155

/** Whether to use network mode */

156

networkMode?: 'online' | 'always' | 'offlineFirst';

157

}

158

```

159

160

### Mutation Result Interface

161

162

Signal-based result object providing reactive access to mutation state.

163

164

```typescript { .api }

165

interface CreateMutationResult<TData, TError, TVariables, TContext> {

166

/** Function to trigger the mutation */

167

mutate: CreateMutateFunction<TData, TError, TVariables, TContext>;

168

/** Async function to trigger the mutation and return a promise */

169

mutateAsync: CreateMutateAsyncFunction<TData, TError, TVariables, TContext>;

170

/** Signal containing the mutation data */

171

data: Signal<TData | undefined>;

172

/** Signal containing any error that occurred */

173

error: Signal<TError | null>;

174

/** Signal containing the variables passed to the mutation */

175

variables: Signal<TVariables | undefined>;

176

/** Signal indicating if mutation is currently running */

177

isPending: Signal<boolean>;

178

/** Signal indicating if mutation completed successfully */

179

isSuccess: Signal<boolean>;

180

/** Signal indicating if mutation resulted in error */

181

isError: Signal<boolean>;

182

/** Signal indicating if mutation is idle (not yet called) */

183

isIdle: Signal<boolean>;

184

/** Signal containing mutation status */

185

status: Signal<'idle' | 'pending' | 'error' | 'success'>;

186

/** Signal containing current failure count */

187

failureCount: Signal<number>;

188

/** Signal containing failure reason */

189

failureReason: Signal<TError | null>;

190

191

// Type narrowing methods

192

isSuccess(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;

193

isError(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;

194

isPending(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;

195

isIdle(this: CreateMutationResult<TData, TError, TVariables, TContext>): this is CreateMutationResult<TData, TError, TVariables, TContext>;

196

}

197

```

198

199

### Mutation Function Types

200

201

Type definitions for mutation functions.

202

203

```typescript { .api }

204

type CreateMutateFunction<TData, TError, TVariables, TContext> = (

205

variables: TVariables,

206

options?: {

207

onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;

208

onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void;

209

onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void;

210

}

211

) => void;

212

213

type CreateMutateAsyncFunction<TData, TError, TVariables, TContext> = (

214

variables: TVariables,

215

options?: {

216

onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;

217

onError?: (error: TError, variables: TVariables, context: TContext | undefined) => void;

218

onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void;

219

}

220

) => Promise<TData>;

221

222

type MutationFunction<TData, TVariables> = (variables: TVariables) => Promise<TData>;

223

```

224

225

### Options Configuration

226

227

Configuration interface for injectMutation behavior.

228

229

```typescript { .api }

230

interface InjectMutationOptions {

231

/**

232

* The Injector in which to create the mutation.

233

* If not provided, the current injection context will be used instead (via inject).

234

*/

235

injector?: Injector;

236

}

237

```

238

239

## Advanced Usage Patterns

240

241

### Optimistic Updates

242

243

```typescript

244

@Component({})

245

export class OptimisticUpdateComponent {

246

#http = inject(HttpClient);

247

#queryClient = inject(QueryClient);

248

249

updateTodoMutation = injectMutation(() => ({

250

mutationFn: (data: { id: number; completed: boolean }) =>

251

this.#http.patch<Todo>(`/api/todos/${data.id}`, { completed: data.completed }),

252

253

onMutate: async (variables) => {

254

// Cancel any outgoing refetches so they don't overwrite optimistic update

255

await this.#queryClient.cancelQueries({ queryKey: ['todos'] });

256

257

// Snapshot previous value

258

const previousTodos = this.#queryClient.getQueryData<Todo[]>(['todos']);

259

260

// Optimistically update the cache

261

this.#queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>

262

old.map(todo =>

263

todo.id === variables.id

264

? { ...todo, completed: variables.completed }

265

: todo

266

)

267

);

268

269

return { previousTodos };

270

},

271

272

onError: (error, variables, context) => {

273

// Rollback to previous state on error

274

if (context?.previousTodos) {

275

this.#queryClient.setQueryData(['todos'], context.previousTodos);

276

}

277

},

278

279

onSettled: () => {

280

// Always refetch after error or success to ensure we have latest data

281

this.#queryClient.invalidateQueries({ queryKey: ['todos'] });

282

}

283

}));

284

}

285

```

286

287

### Sequential Mutations

288

289

```typescript

290

@Component({})

291

export class SequentialMutationsComponent {

292

#http = inject(HttpClient);

293

#queryClient = inject(QueryClient);

294

295

createUserAndProfileMutation = injectMutation(() => ({

296

mutationFn: async (userData: CreateUserData) => {

297

// First create the user

298

const user = await this.#http.post<User>('/api/users', {

299

name: userData.name,

300

email: userData.email

301

}).toPromise();

302

303

// Then create their profile

304

const profile = await this.#http.post<Profile>('/api/profiles', {

305

userId: user.id,

306

bio: userData.bio,

307

avatar: userData.avatar

308

}).toPromise();

309

310

return { user, profile };

311

},

312

313

onSuccess: ({ user, profile }) => {

314

// Update cache with both entities

315

this.#queryClient.setQueryData(['user', user.id], user);

316

this.#queryClient.setQueryData(['profile', user.id], profile);

317

this.#queryClient.invalidateQueries({ queryKey: ['users'] });

318

}

319

}));

320

}

321

```

322

323

### Error Recovery

324

325

```typescript

326

@Component({})

327

export class ErrorRecoveryComponent {

328

#http = inject(HttpClient);

329

330

retryableMutation = injectMutation(() => ({

331

mutationFn: (data: any) => this.#http.post('/api/data', data),

332

333

retry: (failureCount, error: any) => {

334

// Retry up to 3 times, but only for specific errors

335

if (failureCount < 3) {

336

// Retry on network errors or 5xx server errors

337

return !error.status || error.status >= 500;

338

}

339

return false;

340

},

341

342

retryDelay: (attemptIndex) => {

343

// Exponential backoff with jitter

344

const baseDelay = Math.min(1000 * 2 ** attemptIndex, 30000);

345

return baseDelay + Math.random() * 1000;

346

},

347

348

onError: (error, variables, context) => {

349

// Log error for monitoring

350

console.error('Mutation failed after retries:', error);

351

352

// Could show user-friendly error message

353

this.showErrorMessage(error);

354

}

355

}));

356

357

private showErrorMessage(error: any) {

358

// Implementation for showing user-friendly errors

359

}

360

}

361

```

362

363

### Global Mutation Defaults

364

365

```typescript

366

// Can be set up in app configuration

367

const queryClient = new QueryClient({

368

defaultOptions: {

369

mutations: {

370

retry: 1,

371

throwOnError: false,

372

onError: (error) => {

373

// Global error handling for all mutations

374

console.error('Mutation error:', error);

375

}

376

}

377

}

378

});

379

```