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

multi-query-operations.mddocs/

0

# Multi-Query Operations

1

2

Functions for handling multiple queries simultaneously with type-safe results and combined operations using Angular signals.

3

4

## Capabilities

5

6

### Inject Queries

7

8

Handles multiple queries at once with type-safe results and optional result combination.

9

10

```typescript { .api }

11

/**

12

* Injects multiple queries and returns their combined results.

13

* @param config - Configuration object with queries array and optional combine function

14

* @param injector - Optional custom injector

15

* @returns Signal containing combined results from all queries

16

*/

17

function injectQueries<T extends Array<any>, TCombinedResult = QueriesResults<T>>(

18

config: {

19

queries: Signal<[...QueriesOptions<T>]>;

20

combine?: (result: QueriesResults<T>) => TCombinedResult;

21

},

22

injector?: Injector

23

): Signal<TCombinedResult>;

24

```

25

26

**Usage Examples:**

27

28

```typescript

29

import { injectQueries } from "@tanstack/angular-query-experimental";

30

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

31

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

32

33

@Component({

34

selector: 'app-dashboard',

35

template: `

36

<div class="dashboard">

37

<div *ngIf="dashboardData.isLoading" class="loading">

38

Loading dashboard...

39

</div>

40

41

<div *ngIf="dashboardData.hasError" class="error">

42

Some data failed to load

43

</div>

44

45

<div *ngIf="dashboardData.data" class="dashboard-content">

46

<div class="user-info">

47

<h2>{{ dashboardData.data.user?.name }}</h2>

48

<p>{{ dashboardData.data.user?.email }}</p>

49

</div>

50

51

<div class="stats">

52

<div class="stat-item">

53

<label>Posts</label>

54

<span>{{ dashboardData.data.posts?.length || 0 }}</span>

55

</div>

56

<div class="stat-item">

57

<label>Notifications</label>

58

<span>{{ dashboardData.data.notifications?.length || 0 }}</span>

59

</div>

60

</div>

61

</div>

62

</div>

63

`

64

})

65

export class DashboardComponent {

66

#http = inject(HttpClient);

67

68

// Multiple queries with combined result

69

private queries = signal([

70

{

71

queryKey: ['user'] as const,

72

queryFn: () => this.#http.get<User>('/api/user')

73

},

74

{

75

queryKey: ['posts'] as const,

76

queryFn: () => this.#http.get<Post[]>('/api/posts')

77

},

78

{

79

queryKey: ['notifications'] as const,

80

queryFn: () => this.#http.get<Notification[]>('/api/notifications')

81

}

82

]);

83

84

dashboardData = injectQueries({

85

queries: this.queries,

86

combine: (results) => {

87

const [userResult, postsResult, notificationsResult] = results;

88

89

return {

90

isLoading: results.some(result => result.isPending),

91

hasError: results.some(result => result.isError),

92

data: results.every(result => result.isSuccess) ? {

93

user: userResult.data,

94

posts: postsResult.data,

95

notifications: notificationsResult.data

96

} : null

97

};

98

}

99

});

100

}

101

102

interface User {

103

id: number;

104

name: string;

105

email: string;

106

}

107

108

interface Post {

109

id: number;

110

title: string;

111

content: string;

112

}

113

114

interface Notification {

115

id: number;

116

message: string;

117

read: boolean;

118

}

119

```

120

121

### Inject Mutation State

122

123

Tracks the state of all mutations across the application.

124

125

```typescript { .api }

126

/**

127

* Injects a signal that tracks the state of all mutations.

128

* @param injectMutationStateFn - A function that returns mutation state options

129

* @param options - Additional configuration including custom injector

130

* @returns The signal that tracks the state of all mutations

131

*/

132

function injectMutationState<TResult = MutationState>(

133

injectMutationStateFn?: () => MutationStateOptions<TResult>,

134

options?: InjectMutationStateOptions

135

): Signal<Array<TResult>>;

136

137

interface MutationStateOptions<TResult = MutationState> {

138

/** Filters to apply to mutations */

139

filters?: MutationFilters;

140

/** Function to transform each mutation state */

141

select?: (mutation: Mutation) => TResult;

142

}

143

144

interface InjectMutationStateOptions {

145

/** The Injector in which to create the mutation state signal */

146

injector?: Injector;

147

}

148

149

interface MutationState {

150

context: unknown;

151

data: unknown;

152

error: unknown;

153

failureCount: number;

154

failureReason: unknown;

155

isPaused: boolean;

156

status: 'idle' | 'pending' | 'error' | 'success';

157

variables: unknown;

158

submittedAt: number;

159

}

160

```

161

162

**Usage Examples:**

163

164

```typescript

165

import { injectMutationState } from "@tanstack/angular-query-experimental";

166

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

167

168

@Component({

169

selector: 'app-operations-monitor',

170

template: `

171

<div class="operations-monitor">

172

<h3>Active Operations</h3>

173

174

<div *ngIf="activeMutations().length === 0" class="no-operations">

175

No active operations

176

</div>

177

178

<div

179

*ngFor="let mutation of activeMutations()"

180

class="operation-item"

181

[ngClass]="'status-' + mutation.status"

182

>

183

<span class="operation-name">{{ mutation.name }}</span>

184

<span class="operation-status">{{ mutation.status }}</span>

185

<div *ngIf="mutation.error" class="operation-error">

186

{{ mutation.error }}

187

</div>

188

</div>

189

190

<div class="summary">

191

<div>Pending: {{ pendingCount() }}</div>

192

<div>Failed: {{ failedCount() }}</div>

193

<div>Successful: {{ successCount() }}</div>

194

</div>

195

</div>

196

`

197

})

198

export class OperationsMonitorComponent {

199

// All mutation states with custom selection

200

allMutations = injectMutationState(() => ({

201

select: (mutation) => ({

202

id: mutation.mutationId,

203

name: this.getMutationName(mutation),

204

status: mutation.state.status,

205

error: mutation.state.error?.message || null,

206

variables: mutation.state.variables,

207

submittedAt: mutation.state.submittedAt

208

})

209

}));

210

211

// Only active (pending) mutations

212

activeMutations = injectMutationState(() => ({

213

filters: { status: 'pending' },

214

select: (mutation) => ({

215

name: this.getMutationName(mutation),

216

status: mutation.state.status,

217

error: mutation.state.error?.message || null

218

})

219

}));

220

221

// Computed summary statistics

222

pendingCount = computed(() =>

223

this.allMutations().filter(m => m.status === 'pending').length

224

);

225

226

failedCount = computed(() =>

227

this.allMutations().filter(m => m.status === 'error').length

228

);

229

230

successCount = computed(() =>

231

this.allMutations().filter(m => m.status === 'success').length

232

);

233

234

private getMutationName(mutation: any): string {

235

const key = mutation.options.mutationKey?.[0];

236

return typeof key === 'string' ? key : 'Unknown Operation';

237

}

238

}

239

240

@Component({

241

selector: 'app-recent-changes',

242

template: `

243

<div class="recent-changes">

244

<h4>Recent Changes</h4>

245

<div

246

*ngFor="let change of recentChanges()"

247

class="change-item"

248

>

249

<span class="change-type">{{ change.type }}</span>

250

<span class="change-time">{{ formatTime(change.time) }}</span>

251

<div class="change-details">{{ change.details }}</div>

252

</div>

253

</div>

254

`

255

})

256

export class RecentChangesComponent {

257

// Track successful mutations for audit trail

258

recentChanges = injectMutationState(() => ({

259

filters: { status: 'success' },

260

select: (mutation) => ({

261

type: this.getMutationType(mutation),

262

time: mutation.state.submittedAt,

263

details: this.getMutationDetails(mutation),

264

data: mutation.state.data

265

})

266

}));

267

268

private getMutationType(mutation: any): string {

269

const key = mutation.options.mutationKey?.[0];

270

if (typeof key === 'string') {

271

if (key.includes('create')) return 'Created';

272

if (key.includes('update')) return 'Updated';

273

if (key.includes('delete')) return 'Deleted';

274

}

275

return 'Modified';

276

}

277

278

private getMutationDetails(mutation: any): string {

279

const variables = mutation.state.variables;

280

if (variables && typeof variables === 'object') {

281

return JSON.stringify(variables, null, 2);

282

}

283

return 'No details available';

284

}

285

286

formatTime(timestamp: number): string {

287

return new Date(timestamp).toLocaleTimeString();

288

}

289

}

290

```

291

292

### Type Definitions

293

294

Comprehensive type definitions for multi-query operations.

295

296

```typescript { .api }

297

/**

298

* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param

299

*/

300

type QueriesOptions<

301

T extends Array<any>,

302

TResult extends Array<any> = [],

303

TDepth extends ReadonlyArray<number> = []

304

> = TDepth['length'] extends 20 // MAXIMUM_DEPTH

305

? Array<QueryObserverOptionsForCreateQueries>

306

: T extends []

307

? []

308

: T extends [infer Head]

309

? [...TResult, GetOptions<Head>]

310

: T extends [infer Head, ...infer Tail]

311

? QueriesOptions<[...Tail], [...TResult, GetOptions<Head>], [...TDepth, 1]>

312

: ReadonlyArray<unknown> extends T

313

? T

314

: T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, infer TQueryKey>>

315

? Array<QueryObserverOptionsForCreateQueries<TQueryFnData, TError, TData, TQueryKey>>

316

: Array<QueryObserverOptionsForCreateQueries>;

317

318

/**

319

* QueriesResults reducer recursively maps type param to results

320

*/

321

type QueriesResults<

322

T extends Array<any>,

323

TResult extends Array<any> = [],

324

TDepth extends ReadonlyArray<number> = []

325

> = TDepth['length'] extends 20 // MAXIMUM_DEPTH

326

? Array<QueryObserverResult>

327

: T extends []

328

? []

329

: T extends [infer Head]

330

? [...TResult, GetResults<Head>]

331

: T extends [infer Head, ...infer Tail]

332

? QueriesResults<[...Tail], [...TResult, GetResults<Head>], [...TDepth, 1]>

333

: T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, any>>

334

? Array<QueryObserverResult<unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError>>

335

: Array<QueryObserverResult>;

336

```

337

338

## Advanced Usage Patterns

339

340

### Dynamic Query Arrays

341

342

```typescript

343

@Component({})

344

export class DynamicQueriesComponent {

345

#http = inject(HttpClient);

346

userIds = signal([1, 2, 3, 4, 5]);

347

348

// Dynamic queries based on user IDs

349

userQueries = computed(() => {

350

return this.userIds().map(id => ({

351

queryKey: ['user', id] as const,

352

queryFn: () => this.#http.get<User>(`/api/users/${id}`)

353

}));

354

});

355

356

users = injectQueries({

357

queries: this.userQueries,

358

combine: (results) => {

359

return {

360

users: results.map(result => result.data).filter(Boolean),

361

loading: results.some(result => result.isPending),

362

errors: results.filter(result => result.isError).map(result => result.error)

363

};

364

}

365

});

366

367

addUser(id: number) {

368

this.userIds.update(ids => [...ids, id]);

369

}

370

371

removeUser(id: number) {

372

this.userIds.update(ids => ids.filter(existingId => existingId !== id));

373

}

374

}

375

```

376

377

### Conditional Queries

378

379

```typescript

380

@Component({})

381

export class ConditionalQueriesComponent {

382

#http = inject(HttpClient);

383

384

showAdvanced = signal(false);

385

userId = signal(1);

386

387

// Conditionally include queries

388

queries = computed(() => {

389

const baseQueries = [

390

{

391

queryKey: ['user', this.userId()] as const,

392

queryFn: () => this.#http.get<User>(`/api/users/${this.userId()}`)

393

},

394

{

395

queryKey: ['posts', this.userId()] as const,

396

queryFn: () => this.#http.get<Post[]>(`/api/users/${this.userId()}/posts`)

397

}

398

];

399

400

if (this.showAdvanced()) {

401

baseQueries.push(

402

{

403

queryKey: ['analytics', this.userId()] as const,

404

queryFn: () => this.#http.get<Analytics>(`/api/users/${this.userId()}/analytics`)

405

},

406

{

407

queryKey: ['permissions', this.userId()] as const,

408

queryFn: () => this.#http.get<Permissions>(`/api/users/${this.userId()}/permissions`)

409

}

410

);

411

}

412

413

return baseQueries;

414

});

415

416

data = injectQueries({

417

queries: this.queries,

418

combine: (results) => ({

419

isLoading: results.some(r => r.isPending),

420

hasError: results.some(r => r.isError),

421

user: results[0]?.data,

422

posts: results[1]?.data,

423

analytics: results[2]?.data,

424

permissions: results[3]?.data

425

})

426

});

427

}

428

```

429

430

### Error Handling and Retry Logic

431

432

```typescript

433

@Component({})

434

export class RobustQueriesComponent {

435

#http = inject(HttpClient);

436

437

criticalQueries = signal([

438

{

439

queryKey: ['critical-data'] as const,

440

queryFn: () => this.#http.get<CriticalData>('/api/critical'),

441

retry: 3,

442

retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000)

443

},

444

{

445

queryKey: ['user-preferences'] as const,

446

queryFn: () => this.#http.get<UserPreferences>('/api/user/preferences'),

447

retry: 1

448

}

449

]);

450

451

appData = injectQueries({

452

queries: this.criticalQueries,

453

combine: (results) => {

454

const [criticalResult, preferencesResult] = results;

455

456

// Handle different error scenarios

457

if (criticalResult.isError && criticalResult.failureCount >= 3) {

458

return {

459

status: 'critical-error',

460

error: criticalResult.error,

461

canRetry: false

462

};

463

}

464

465

if (results.some(r => r.isLoading)) {

466

return {

467

status: 'loading',

468

progress: results.filter(r => r.isSuccess).length / results.length

469

};

470

}

471

472

return {

473

status: 'success',

474

criticalData: criticalResult.data,

475

preferences: preferencesResult.data,

476

hasPartialData: criticalResult.isSuccess || preferencesResult.isSuccess

477

};

478

}

479

});

480

}

481

```

482

483

### Performance Optimization

484

485

```typescript

486

@Component({})

487

export class OptimizedQueriesComponent {

488

#http = inject(HttpClient);

489

490

// Memoized queries to prevent unnecessary recreations

491

queries = computed(() => {

492

return [

493

{

494

queryKey: ['expensive-computation'] as const,

495

queryFn: () => this.#http.get('/api/expensive'),

496

staleTime: 10 * 60 * 1000, // 10 minutes

497

gcTime: 30 * 60 * 1000 // 30 minutes

498

},

499

{

500

queryKey: ['realtime-data'] as const,

501

queryFn: () => this.#http.get('/api/realtime'),

502

staleTime: 0, // Always fresh

503

refetchInterval: 5000 // 5 seconds

504

}

505

];

506

});

507

508

optimizedData = injectQueries({

509

queries: this.queries,

510

combine: (results) => {

511

// Only recalculate when necessary

512

const memoizedResult = {

513

timestamp: Date.now(),

514

data: results.map(r => r.data),

515

isStale: results.some(r => r.isStale)

516

};

517

518

return memoizedResult;

519

}

520

});

521

}

522

```