Signals for managing, caching and syncing asynchronous and remote data in Angular
—
Functions for handling multiple queries simultaneously with type-safe results and combined operations using Angular signals.
Handles multiple queries at once with type-safe results and optional result combination.
/**
* Injects multiple queries and returns their combined results.
* @param config - Configuration object with queries array and optional combine function
* @param injector - Optional custom injector
* @returns Signal containing combined results from all queries
*/
function injectQueries<T extends Array<any>, TCombinedResult = QueriesResults<T>>(
config: {
queries: Signal<[...QueriesOptions<T>]>;
combine?: (result: QueriesResults<T>) => TCombinedResult;
},
injector?: Injector
): Signal<TCombinedResult>;Usage Examples:
import { injectQueries } from "@tanstack/angular-query-experimental";
import { Component, inject, signal } from "@angular/core";
import { HttpClient } from "@angular/common/http";
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<div *ngIf="dashboardData.isLoading" class="loading">
Loading dashboard...
</div>
<div *ngIf="dashboardData.hasError" class="error">
Some data failed to load
</div>
<div *ngIf="dashboardData.data" class="dashboard-content">
<div class="user-info">
<h2>{{ dashboardData.data.user?.name }}</h2>
<p>{{ dashboardData.data.user?.email }}</p>
</div>
<div class="stats">
<div class="stat-item">
<label>Posts</label>
<span>{{ dashboardData.data.posts?.length || 0 }}</span>
</div>
<div class="stat-item">
<label>Notifications</label>
<span>{{ dashboardData.data.notifications?.length || 0 }}</span>
</div>
</div>
</div>
</div>
`
})
export class DashboardComponent {
#http = inject(HttpClient);
// Multiple queries with combined result
private queries = signal([
{
queryKey: ['user'] as const,
queryFn: () => this.#http.get<User>('/api/user')
},
{
queryKey: ['posts'] as const,
queryFn: () => this.#http.get<Post[]>('/api/posts')
},
{
queryKey: ['notifications'] as const,
queryFn: () => this.#http.get<Notification[]>('/api/notifications')
}
]);
dashboardData = injectQueries({
queries: this.queries,
combine: (results) => {
const [userResult, postsResult, notificationsResult] = results;
return {
isLoading: results.some(result => result.isPending),
hasError: results.some(result => result.isError),
data: results.every(result => result.isSuccess) ? {
user: userResult.data,
posts: postsResult.data,
notifications: notificationsResult.data
} : null
};
}
});
}
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
content: string;
}
interface Notification {
id: number;
message: string;
read: boolean;
}Tracks the state of all mutations across the application.
/**
* Injects a signal that tracks the state of all mutations.
* @param injectMutationStateFn - A function that returns mutation state options
* @param options - Additional configuration including custom injector
* @returns The signal that tracks the state of all mutations
*/
function injectMutationState<TResult = MutationState>(
injectMutationStateFn?: () => MutationStateOptions<TResult>,
options?: InjectMutationStateOptions
): Signal<Array<TResult>>;
interface MutationStateOptions<TResult = MutationState> {
/** Filters to apply to mutations */
filters?: MutationFilters;
/** Function to transform each mutation state */
select?: (mutation: Mutation) => TResult;
}
interface InjectMutationStateOptions {
/** The Injector in which to create the mutation state signal */
injector?: Injector;
}
interface MutationState {
context: unknown;
data: unknown;
error: unknown;
failureCount: number;
failureReason: unknown;
isPaused: boolean;
status: 'idle' | 'pending' | 'error' | 'success';
variables: unknown;
submittedAt: number;
}Usage Examples:
import { injectMutationState } from "@tanstack/angular-query-experimental";
import { Component, computed } from "@angular/core";
@Component({
selector: 'app-operations-monitor',
template: `
<div class="operations-monitor">
<h3>Active Operations</h3>
<div *ngIf="activeMutations().length === 0" class="no-operations">
No active operations
</div>
<div
*ngFor="let mutation of activeMutations()"
class="operation-item"
[ngClass]="'status-' + mutation.status"
>
<span class="operation-name">{{ mutation.name }}</span>
<span class="operation-status">{{ mutation.status }}</span>
<div *ngIf="mutation.error" class="operation-error">
{{ mutation.error }}
</div>
</div>
<div class="summary">
<div>Pending: {{ pendingCount() }}</div>
<div>Failed: {{ failedCount() }}</div>
<div>Successful: {{ successCount() }}</div>
</div>
</div>
`
})
export class OperationsMonitorComponent {
// All mutation states with custom selection
allMutations = injectMutationState(() => ({
select: (mutation) => ({
id: mutation.mutationId,
name: this.getMutationName(mutation),
status: mutation.state.status,
error: mutation.state.error?.message || null,
variables: mutation.state.variables,
submittedAt: mutation.state.submittedAt
})
}));
// Only active (pending) mutations
activeMutations = injectMutationState(() => ({
filters: { status: 'pending' },
select: (mutation) => ({
name: this.getMutationName(mutation),
status: mutation.state.status,
error: mutation.state.error?.message || null
})
}));
// Computed summary statistics
pendingCount = computed(() =>
this.allMutations().filter(m => m.status === 'pending').length
);
failedCount = computed(() =>
this.allMutations().filter(m => m.status === 'error').length
);
successCount = computed(() =>
this.allMutations().filter(m => m.status === 'success').length
);
private getMutationName(mutation: any): string {
const key = mutation.options.mutationKey?.[0];
return typeof key === 'string' ? key : 'Unknown Operation';
}
}
@Component({
selector: 'app-recent-changes',
template: `
<div class="recent-changes">
<h4>Recent Changes</h4>
<div
*ngFor="let change of recentChanges()"
class="change-item"
>
<span class="change-type">{{ change.type }}</span>
<span class="change-time">{{ formatTime(change.time) }}</span>
<div class="change-details">{{ change.details }}</div>
</div>
</div>
`
})
export class RecentChangesComponent {
// Track successful mutations for audit trail
recentChanges = injectMutationState(() => ({
filters: { status: 'success' },
select: (mutation) => ({
type: this.getMutationType(mutation),
time: mutation.state.submittedAt,
details: this.getMutationDetails(mutation),
data: mutation.state.data
})
}));
private getMutationType(mutation: any): string {
const key = mutation.options.mutationKey?.[0];
if (typeof key === 'string') {
if (key.includes('create')) return 'Created';
if (key.includes('update')) return 'Updated';
if (key.includes('delete')) return 'Deleted';
}
return 'Modified';
}
private getMutationDetails(mutation: any): string {
const variables = mutation.state.variables;
if (variables && typeof variables === 'object') {
return JSON.stringify(variables, null, 2);
}
return 'No details available';
}
formatTime(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString();
}
}Comprehensive type definitions for multi-query operations.
/**
* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
*/
type QueriesOptions<
T extends Array<any>,
TResult extends Array<any> = [],
TDepth extends ReadonlyArray<number> = []
> = TDepth['length'] extends 20 // MAXIMUM_DEPTH
? Array<QueryObserverOptionsForCreateQueries>
: T extends []
? []
: T extends [infer Head]
? [...TResult, GetOptions<Head>]
: T extends [infer Head, ...infer Tail]
? QueriesOptions<[...Tail], [...TResult, GetOptions<Head>], [...TDepth, 1]>
: ReadonlyArray<unknown> extends T
? T
: T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, infer TQueryKey>>
? Array<QueryObserverOptionsForCreateQueries<TQueryFnData, TError, TData, TQueryKey>>
: Array<QueryObserverOptionsForCreateQueries>;
/**
* QueriesResults reducer recursively maps type param to results
*/
type QueriesResults<
T extends Array<any>,
TResult extends Array<any> = [],
TDepth extends ReadonlyArray<number> = []
> = TDepth['length'] extends 20 // MAXIMUM_DEPTH
? Array<QueryObserverResult>
: T extends []
? []
: T extends [infer Head]
? [...TResult, GetResults<Head>]
: T extends [infer Head, ...infer Tail]
? QueriesResults<[...Tail], [...TResult, GetResults<Head>], [...TDepth, 1]>
: T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, any>>
? Array<QueryObserverResult<unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError>>
: Array<QueryObserverResult>;@Component({})
export class DynamicQueriesComponent {
#http = inject(HttpClient);
userIds = signal([1, 2, 3, 4, 5]);
// Dynamic queries based on user IDs
userQueries = computed(() => {
return this.userIds().map(id => ({
queryKey: ['user', id] as const,
queryFn: () => this.#http.get<User>(`/api/users/${id}`)
}));
});
users = injectQueries({
queries: this.userQueries,
combine: (results) => {
return {
users: results.map(result => result.data).filter(Boolean),
loading: results.some(result => result.isPending),
errors: results.filter(result => result.isError).map(result => result.error)
};
}
});
addUser(id: number) {
this.userIds.update(ids => [...ids, id]);
}
removeUser(id: number) {
this.userIds.update(ids => ids.filter(existingId => existingId !== id));
}
}@Component({})
export class ConditionalQueriesComponent {
#http = inject(HttpClient);
showAdvanced = signal(false);
userId = signal(1);
// Conditionally include queries
queries = computed(() => {
const baseQueries = [
{
queryKey: ['user', this.userId()] as const,
queryFn: () => this.#http.get<User>(`/api/users/${this.userId()}`)
},
{
queryKey: ['posts', this.userId()] as const,
queryFn: () => this.#http.get<Post[]>(`/api/users/${this.userId()}/posts`)
}
];
if (this.showAdvanced()) {
baseQueries.push(
{
queryKey: ['analytics', this.userId()] as const,
queryFn: () => this.#http.get<Analytics>(`/api/users/${this.userId()}/analytics`)
},
{
queryKey: ['permissions', this.userId()] as const,
queryFn: () => this.#http.get<Permissions>(`/api/users/${this.userId()}/permissions`)
}
);
}
return baseQueries;
});
data = injectQueries({
queries: this.queries,
combine: (results) => ({
isLoading: results.some(r => r.isPending),
hasError: results.some(r => r.isError),
user: results[0]?.data,
posts: results[1]?.data,
analytics: results[2]?.data,
permissions: results[3]?.data
})
});
}@Component({})
export class RobustQueriesComponent {
#http = inject(HttpClient);
criticalQueries = signal([
{
queryKey: ['critical-data'] as const,
queryFn: () => this.#http.get<CriticalData>('/api/critical'),
retry: 3,
retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000)
},
{
queryKey: ['user-preferences'] as const,
queryFn: () => this.#http.get<UserPreferences>('/api/user/preferences'),
retry: 1
}
]);
appData = injectQueries({
queries: this.criticalQueries,
combine: (results) => {
const [criticalResult, preferencesResult] = results;
// Handle different error scenarios
if (criticalResult.isError && criticalResult.failureCount >= 3) {
return {
status: 'critical-error',
error: criticalResult.error,
canRetry: false
};
}
if (results.some(r => r.isLoading)) {
return {
status: 'loading',
progress: results.filter(r => r.isSuccess).length / results.length
};
}
return {
status: 'success',
criticalData: criticalResult.data,
preferences: preferencesResult.data,
hasPartialData: criticalResult.isSuccess || preferencesResult.isSuccess
};
}
});
}@Component({})
export class OptimizedQueriesComponent {
#http = inject(HttpClient);
// Memoized queries to prevent unnecessary recreations
queries = computed(() => {
return [
{
queryKey: ['expensive-computation'] as const,
queryFn: () => this.#http.get('/api/expensive'),
staleTime: 10 * 60 * 1000, // 10 minutes
gcTime: 30 * 60 * 1000 // 30 minutes
},
{
queryKey: ['realtime-data'] as const,
queryFn: () => this.#http.get('/api/realtime'),
staleTime: 0, // Always fresh
refetchInterval: 5000 // 5 seconds
}
];
});
optimizedData = injectQueries({
queries: this.queries,
combine: (results) => {
// Only recalculate when necessary
const memoizedResult = {
timestamp: Date.now(),
data: results.map(r => r.data),
isStale: results.some(r => r.isStale)
};
return memoizedResult;
}
});
}Install with Tessl CLI
npx tessl i tessl/npm-tanstack--angular-query-experimental