Signals for managing, caching and syncing asynchronous and remote data in Angular
—
Functions for monitoring query and mutation states across the application for loading indicators and debugging using Angular signals.
Tracks the number of queries that are currently loading or fetching in the background.
/**
* Injects a signal that tracks the number of queries that your application is loading or
* fetching in the background. Can be used for app-wide loading indicators.
* @param filters - The filters to apply to the query
* @param options - Additional configuration including custom injector
* @returns signal with number of loading or fetching queries
*/
function injectIsFetching(
filters?: QueryFilters,
options?: InjectIsFetchingOptions
): Signal<number>;
interface InjectIsFetchingOptions {
/** The Injector in which to create the isFetching signal */
injector?: Injector;
}
interface QueryFilters {
/** Filter by query key */
queryKey?: QueryKey;
/** Filter by exact query key match */
exact?: boolean;
/** Filter by query type */
type?: 'active' | 'inactive' | 'all';
/** Filter by stale status */
stale?: boolean;
/** Filter by fetch status */
fetchStatus?: 'fetching' | 'paused' | 'idle';
/** Custom predicate function */
predicate?: (query: Query) => boolean;
}Usage Examples:
import { injectIsFetching } from "@tanstack/angular-query-experimental";
import { Component } from "@angular/core";
@Component({
selector: 'app-global-loading',
template: `
<div
class="global-loading-indicator"
[class.visible]="isFetching() > 0"
>
Loading {{ isFetching() }} {{ isFetching() === 1 ? 'query' : 'queries' }}...
</div>
`
})
export class GlobalLoadingComponent {
// Track all fetching queries
isFetching = injectIsFetching();
// Track only specific queries
userQueriesFetching = injectIsFetching({
queryKey: ['users'],
exact: false // Include all queries that start with ['users']
});
// Track only active queries
activeFetching = injectIsFetching({
type: 'active'
});
}
@Component({
selector: 'app-section-loading',
template: `
<div class="section">
<h2>User Management</h2>
<div *ngIf="userSectionLoading() > 0" class="loading-bar">
Loading user data...
</div>
<!-- User content -->
</div>
`
})
export class UserSectionComponent {
// Track only user-related queries
userSectionLoading = injectIsFetching({
predicate: (query) => {
const key = query.queryKey[0];
return typeof key === 'string' &&
(key.includes('user') || key.includes('profile') || key.includes('account'));
}
});
}Tracks the number of mutations that are currently running.
/**
* Injects a signal that tracks the number of mutations that your application is fetching.
* Can be used for app-wide loading indicators.
* @param filters - The filters to apply to the mutation
* @param options - Additional configuration including custom injector
* @returns signal with number of fetching mutations
*/
function injectIsMutating(
filters?: MutationFilters,
options?: InjectIsMutatingOptions
): Signal<number>;
interface InjectIsMutatingOptions {
/** The Injector in which to create the isMutating signal */
injector?: Injector;
}
interface MutationFilters {
/** Filter by mutation key */
mutationKey?: MutationKey;
/** Filter by exact mutation key match */
exact?: boolean;
/** Filter by mutation status */
status?: 'idle' | 'pending' | 'error' | 'success';
/** Custom predicate function */
predicate?: (mutation: Mutation) => boolean;
}Usage Examples:
import { injectIsMutating } from "@tanstack/angular-query-experimental";
import { Component } from "@angular/core";
@Component({
selector: 'app-save-indicator',
template: `
<div
class="save-indicator"
[class.saving]="isMutating() > 0"
>
<span *ngIf="isMutating() > 0">
Saving {{ isMutating() }} {{ isMutating() === 1 ? 'item' : 'items' }}...
</span>
<span *ngIf="isMutating() === 0">
All changes saved
</span>
</div>
`
})
export class SaveIndicatorComponent {
// Track all running mutations
isMutating = injectIsMutating();
// Track only user-related mutations
userMutations = injectIsMutating({
predicate: (mutation) => {
const key = mutation.options.mutationKey?.[0];
return typeof key === 'string' && key.startsWith('user');
}
});
// Track only pending mutations
pendingMutations = injectIsMutating({
status: 'pending'
});
}
@Component({
selector: 'app-form-controls',
template: `
<div class="form-actions">
<button
type="submit"
[disabled]="isSubmitting() > 0"
>
{{ isSubmitting() > 0 ? 'Submitting...' : 'Submit' }}
</button>
<button
type="button"
[disabled]="isSubmitting() > 0"
(click)="cancel()"
>
Cancel
</button>
</div>
`
})
export class FormControlsComponent {
// Track form submission mutations
isSubmitting = injectIsMutating({
mutationKey: ['submitForm']
});
}Tracks whether a restore operation is currently in progress, used for persistence scenarios.
/**
* Injects a signal that tracks whether a restore is currently in progress.
* injectQuery and friends also check this internally to avoid race conditions
* between the restore and initializing queries.
* @param options - Options for injectIsRestoring including custom injector
* @returns signal with boolean that indicates whether a restore is in progress
*/
function injectIsRestoring(options?: InjectIsRestoringOptions): Signal<boolean>;
interface InjectIsRestoringOptions {
/** The Injector in which to create the isRestoring signal */
injector?: Injector;
}Usage Examples:
import { injectIsRestoring } from "@tanstack/angular-query-experimental";
import { Component } from "@angular/core";
@Component({
selector: 'app-root',
template: `
<div class="app">
<div *ngIf="isRestoring()" class="restore-overlay">
<div class="restore-message">
Restoring your data...
</div>
</div>
<router-outlet *ngIf="!isRestoring()"></router-outlet>
</div>
`
})
export class AppComponent {
isRestoring = injectIsRestoring();
}
@Component({
selector: 'app-data-manager',
template: `
<div class="data-status">
<div *ngIf="isRestoring()" class="status-item">
<span class="icon">🔄</span>
Restoring cached data...
</div>
<div *ngIf="!isRestoring() && isFetching() > 0" class="status-item">
<span class="icon">📡</span>
Fetching {{ isFetching() }} queries...
</div>
<div *ngIf="!isRestoring() && isMutating() > 0" class="status-item">
<span class="icon">💾</span>
Saving {{ isMutating() }} changes...
</div>
</div>
`
})
export class DataManagerComponent {
isRestoring = injectIsRestoring();
isFetching = injectIsFetching();
isMutating = injectIsMutating();
}@Component({
selector: 'app-network-status',
template: `
<div class="network-status" [ngClass]="statusClass()">
<div class="status-text">{{ statusText() }}</div>
<div class="status-details">
<span *ngIf="isFetching() > 0">{{ isFetching() }} fetching</span>
<span *ngIf="isMutating() > 0">{{ isMutating() }} saving</span>
<span *ngIf="isRestoring()">Restoring</span>
</div>
</div>
`
})
export class NetworkStatusComponent {
isRestoring = injectIsRestoring();
isFetching = injectIsFetching();
isMutating = injectIsMutating();
statusClass = computed(() => {
if (this.isRestoring()) return 'status-restoring';
if (this.isMutating() > 0) return 'status-saving';
if (this.isFetching() > 0) return 'status-loading';
return 'status-idle';
});
statusText = computed(() => {
if (this.isRestoring()) return 'Restoring data...';
if (this.isMutating() > 0) return 'Saving changes...';
if (this.isFetching() > 0) return 'Loading data...';
return 'Ready';
});
}@Component({})
export class FilteredStatusComponent {
// Monitor different types of operations separately
backgroundRefetch = injectIsFetching({
predicate: (query) => query.state.fetchStatus === 'fetching' && !query.state.isLoading
});
initialLoading = injectIsFetching({
predicate: (query) => query.state.isLoading
});
criticalMutations = injectIsMutating({
predicate: (mutation) => {
const key = mutation.options.mutationKey?.[0];
return typeof key === 'string' &&
['deleteUser', 'submitPayment', 'publishPost'].includes(key);
}
});
// Computed status based on different priorities
overallStatus = computed(() => {
if (this.criticalMutations() > 0) return 'critical-saving';
if (this.initialLoading() > 0) return 'initial-loading';
if (this.backgroundRefetch() > 0) return 'background-update';
return 'idle';
});
}import { Injectable, computed, signal } from '@angular/core';
import { injectIsFetching, injectIsMutating, injectIsRestoring } from '@tanstack/angular-query-experimental';
@Injectable({ providedIn: 'root' })
export class AppStatusService {
private isRestoring = injectIsRestoring();
private isFetching = injectIsFetching();
private isMutating = injectIsMutating();
// Custom status tracking
private _isOnline = signal(navigator.onLine);
private _hasError = signal(false);
// Computed overall app status
readonly appStatus = computed(() => {
if (!this._isOnline()) return 'offline';
if (this._hasError()) return 'error';
if (this.isRestoring()) return 'restoring';
if (this.isMutating() > 0) return 'saving';
if (this.isFetching() > 0) return 'loading';
return 'ready';
});
readonly isBusy = computed(() => {
return this.isRestoring() || this.isFetching() > 0 || this.isMutating() > 0;
});
constructor() {
// Listen for online/offline events
window.addEventListener('online', () => this._isOnline.set(true));
window.addEventListener('offline', () => this._isOnline.set(false));
}
setError(hasError: boolean) {
this._hasError.set(hasError);
}
get statusMessage() {
switch (this.appStatus()) {
case 'offline': return 'You are offline';
case 'error': return 'Something went wrong';
case 'restoring': return 'Restoring your data...';
case 'saving': return 'Saving changes...';
case 'loading': return 'Loading...';
case 'ready': return 'Ready';
default: return '';
}
}
}
// Usage in components
@Component({})
export class SomeComponent {
private statusService = inject(AppStatusService);
appStatus = this.statusService.appStatus;
isBusy = this.statusService.isBusy;
statusMessage = this.statusService.statusMessage;
}@Component({
template: `
<div class="progress-container">
<div class="progress-bar">
<div
class="progress-fill"
[style.width.%]="progressPercentage()"
></div>
</div>
<div class="progress-text">
{{ progressText() }}
</div>
</div>
`
})
export class ProgressTrackingComponent {
private totalOperations = signal(0);
private completedOperations = signal(0);
isFetching = injectIsFetching();
isMutating = injectIsMutating();
progressPercentage = computed(() => {
const total = this.totalOperations();
const completed = this.completedOperations();
return total > 0 ? (completed / total) * 100 : 0;
});
progressText = computed(() => {
const fetching = this.isFetching();
const mutating = this.isMutating();
const total = fetching + mutating;
if (total === 0) return 'All operations complete';
return `${total} operations in progress`;
});
// Methods to update progress (called by parent components)
updateProgress(total: number, completed: number) {
this.totalOperations.set(total);
this.completedOperations.set(completed);
}
}Install with Tessl CLI
npx tessl i tessl/npm-tanstack--angular-query-experimental