Signals for managing, caching and syncing asynchronous and remote data in Angular
—
Core query functionality for fetching and caching data with automatic refetching, stale-while-revalidate patterns, and reactive updates using Angular signals.
Creates a reactive query that automatically fetches data and provides signals for all query states.
/**
* Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
* @param injectQueryFn - A function that returns query options
* @param options - Additional configuration including custom injector
* @returns The query result with signals for reactive access
*/
function injectQuery<TQueryFnData, TError, TData, TQueryKey>(
injectQueryFn: () => CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
options?: InjectQueryOptions
): CreateQueryResult<TData, TError>;
// Overloads for different initial data scenarios
function injectQuery<TQueryFnData, TError, TData, TQueryKey>(
injectQueryFn: () => DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
options?: InjectQueryOptions
): DefinedCreateQueryResult<TData, TError>;
function injectQuery<TQueryFnData, TError, TData, TQueryKey>(
injectQueryFn: () => UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
options?: InjectQueryOptions
): CreateQueryResult<TData, TError>;Usage Examples:
import { injectQuery } from "@tanstack/angular-query-experimental";
import { Component, inject, signal } from "@angular/core";
import { HttpClient } from "@angular/common/http";
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="userQuery.isPending()">Loading user...</div>
<div *ngIf="userQuery.isError()">Error: {{ userQuery.error()?.message }}</div>
<div *ngIf="userQuery.isSuccess()">
<h2>{{ userQuery.data()?.name }}</h2>
<p>{{ userQuery.data()?.email }}</p>
</div>
`
})
export class UserProfileComponent {
#http = inject(HttpClient);
userId = signal(1);
// Basic query
userQuery = injectQuery(() => ({
queryKey: ['user', this.userId()],
queryFn: () => this.#http.get<User>(`/api/users/${this.userId()}`),
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: true
}));
// Query with enabled condition
userDetailsQuery = injectQuery(() => ({
queryKey: ['userDetails', this.userId()],
queryFn: () => this.#http.get<UserDetails>(`/api/users/${this.userId()}/details`),
enabled: !!this.userId() && this.userId() > 0
}));
}
interface User {
id: number;
name: string;
email: string;
}
interface UserDetails {
bio: string;
joinDate: string;
lastLogin: string;
}Comprehensive options for configuring query behavior.
interface CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey> {
/** Unique key for the query, used for caching and invalidation */
queryKey: TQueryKey;
/** Function that returns a promise resolving to the data */
queryFn: QueryFunction<TQueryFnData, TQueryKey>;
/** Whether the query should automatically execute */
enabled?: boolean;
/** Time in milliseconds after which data is considered stale */
staleTime?: number;
/** Time in milliseconds after which unused data is garbage collected */
gcTime?: number;
/** Whether to refetch when window regains focus */
refetchOnWindowFocus?: boolean;
/** Whether to refetch when component reconnects */
refetchOnReconnect?: boolean;
/** Interval in milliseconds for automatic refetching */
refetchInterval?: number;
/** Whether to continue refetching while window is hidden */
refetchIntervalInBackground?: boolean;
/** Number of retry attempts on failure */
retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
/** Delay function for retry attempts */
retryDelay?: number | ((retryAttempt: number, error: TError) => number);
/** Initial data to use while loading */
initialData?: TData | (() => TData);
/** Placeholder data to show while loading */
placeholderData?: TData | ((previousData: TData | undefined) => TData);
/** Function to transform query data */
select?: (data: TQueryFnData) => TData;
/** Whether to throw errors instead of setting error state */
throwOnError?: boolean | ((error: TError) => boolean);
/** Structural sharing to optimize re-renders */
structuralSharing?: boolean | ((oldData: TData | undefined, newData: TData) => TData);
}Signal-based result object providing reactive access to query state.
interface CreateQueryResult<TData, TError> {
/** Signal containing the query data */
data: Signal<TData | undefined>;
/** Signal containing any error that occurred */
error: Signal<TError | null>;
/** Signal indicating if query is currently loading (first time) */
isLoading: Signal<boolean>;
/** Signal indicating if query is pending (loading or fetching) */
isPending: Signal<boolean>;
/** Signal indicating if query completed successfully */
isSuccess: Signal<boolean>;
/** Signal indicating if query resulted in error */
isError: Signal<boolean>;
/** Signal indicating if query is currently fetching */
isFetching: Signal<boolean>;
/** Signal indicating if query is refetching in background */
isRefetching: Signal<boolean>;
/** Signal indicating if query is stale */
isStale: Signal<boolean>;
/** Signal containing query status */
status: Signal<'pending' | 'error' | 'success'>;
/** Signal containing fetch status */
fetchStatus: Signal<'fetching' | 'paused' | 'idle'>;
/** Signal containing timestamp of last successful fetch */
dataUpdatedAt: Signal<number>;
/** Signal containing timestamp of last error */
errorUpdatedAt: Signal<number>;
/** Signal containing current failure count */
failureCount: Signal<number>;
/** Signal containing failure reason */
failureReason: Signal<TError | null>;
// Type narrowing methods
isSuccess(this: CreateQueryResult<TData, TError>): this is CreateQueryResult<TData, TError>;
isError(this: CreateQueryResult<TData, TError>): this is CreateQueryResult<TData, TError>;
isPending(this: CreateQueryResult<TData, TError>): this is CreateQueryResult<TData, TError>;
}
interface DefinedCreateQueryResult<TData, TError> extends CreateQueryResult<TData, TError> {
/** Signal containing the query data (guaranteed to be defined) */
data: Signal<TData>;
}Configuration interface for injectQuery behavior.
interface InjectQueryOptions {
/**
* The Injector in which to create the query.
* If not provided, the current injection context will be used instead (via inject).
*/
injector?: Injector;
}Type definitions for different initial data scenarios.
type UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> =
CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
initialData?: undefined | InitialDataFunction<NonUndefinedGuard<TQueryFnData>> | NonUndefinedGuard<TQueryFnData>;
};
type DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> =
Omit<CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryFn'> & {
initialData: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>);
queryFn?: QueryFunction<TQueryFnData, TQueryKey>;
};@Component({})
export class ReactiveQueryComponent {
#http = inject(HttpClient);
// Signal for reactive dependency
searchTerm = signal('');
// Query automatically updates when searchTerm changes
searchQuery = injectQuery(() => ({
queryKey: ['search', this.searchTerm()],
queryFn: () => this.#http.get<SearchResult[]>(`/api/search?q=${this.searchTerm()}`),
enabled: this.searchTerm().length > 2, // Only search if term is long enough
staleTime: 30000 // 30 seconds
}));
}@Component({})
export class DependentQueriesComponent {
#http = inject(HttpClient);
userQuery = injectQuery(() => ({
queryKey: ['user'],
queryFn: () => this.#http.get<User>('/api/user')
}));
// This query depends on userQuery data
userPostsQuery = injectQuery(() => ({
queryKey: ['posts', this.userQuery.data()?.id],
queryFn: () => this.#http.get<Post[]>(`/api/users/${this.userQuery.data()!.id}/posts`),
enabled: !!this.userQuery.data()?.id
}));
}@Component({})
export class ErrorHandlingComponent {
#http = inject(HttpClient);
dataQuery = injectQuery(() => ({
queryKey: ['data'],
queryFn: () => this.#http.get<Data>('/api/data'),
retry: (failureCount, error) => {
// Only retry on network errors, not 4xx errors
return failureCount < 3 && error.status >= 500;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
throwOnError: false // Handle errors in component instead of throwing
}));
// Access error state
get errorMessage() {
const error = this.dataQuery.error();
return error ? `Failed to load data: ${error.message}` : '';
}
}Install with Tessl CLI
npx tessl i tessl/npm-tanstack--angular-query-experimental