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
```