Signals for managing, caching and syncing asynchronous and remote data in Angular
—
Infinite query functionality for pagination and infinite scrolling with automatic data accumulation and page management using Angular signals.
Creates an infinite query that can load data page by page, perfect for pagination and infinite scrolling scenarios.
/**
* Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
* Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll"
* @param injectInfiniteQueryFn - A function that returns infinite query options
* @param options - Additional configuration including custom injector
* @returns The infinite query result with signals and page management functions
*/
function injectInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
injectInfiniteQueryFn: () => CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
options?: InjectInfiniteQueryOptions
): CreateInfiniteQueryResult<TData, TError>;
// Overloads for different initial data scenarios
function injectInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
injectInfiniteQueryFn: () => DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
options?: InjectInfiniteQueryOptions
): DefinedCreateInfiniteQueryResult<TData, TError>;
function injectInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
injectInfiniteQueryFn: () => UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
options?: InjectInfiniteQueryOptions
): CreateInfiniteQueryResult<TData, TError>;Usage Examples:
import { injectInfiniteQuery } from "@tanstack/angular-query-experimental";
import { Component, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
@Component({
selector: 'app-posts-list',
template: `
<div *ngFor="let page of postsQuery.data()?.pages; let pageIndex = index">
<div *ngFor="let post of page.posts">
<h3>{{ post.title }}</h3>
<p>{{ post.content }}</p>
</div>
</div>
<button
*ngIf="postsQuery.hasNextPage()"
(click)="postsQuery.fetchNextPage()"
[disabled]="postsQuery.isFetchingNextPage()"
>
{{ postsQuery.isFetchingNextPage() ? 'Loading...' : 'Load More' }}
</button>
<div *ngIf="postsQuery.isError()">
Error: {{ postsQuery.error()?.message }}
</div>
`
})
export class PostsListComponent {
#http = inject(HttpClient);
// Basic infinite query for pagination
postsQuery = injectInfiniteQuery(() => ({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) =>
this.#http.get<PostsPage>(`/api/posts?page=${pageParam}&limit=10`),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? lastPage.nextPage : null;
},
getPreviousPageParam: (firstPage, allPages) => {
return firstPage.page > 1 ? firstPage.page - 1 : null;
}
}));
// Infinite query with cursor-based pagination
messagesQuery = injectInfiniteQuery(() => ({
queryKey: ['messages'],
queryFn: ({ pageParam = null }) => {
const url = pageParam
? `/api/messages?cursor=${pageParam}&limit=20`
: '/api/messages?limit=20';
return this.#http.get<MessagesPage>(url);
},
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
staleTime: 5 * 60 * 1000 // 5 minutes
}));
}
interface PostsPage {
posts: Post[];
page: number;
hasMore: boolean;
nextPage: number;
}
interface MessagesPage {
messages: Message[];
nextCursor: string | null;
prevCursor: string | null;
}
interface Post {
id: number;
title: string;
content: string;
}
interface Message {
id: string;
text: string;
timestamp: string;
}Comprehensive options for configuring infinite query behavior.
interface CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> {
/** Unique key for the query, used for caching and invalidation */
queryKey: TQueryKey;
/** Function that returns a promise resolving to the page data */
queryFn: InfiniteQueryFunction<TQueryFnData, TQueryKey, TPageParam>;
/** Initial page parameter for the first page */
initialPageParam: TPageParam;
/** Function to determine the next page parameter */
getNextPageParam: (
lastPage: TQueryFnData,
allPages: TQueryFnData[],
lastPageParam: TPageParam,
allPageParams: TPageParam[]
) => TPageParam | null | undefined;
/** Function to determine the previous page parameter */
getPreviousPageParam?: (
firstPage: TQueryFnData,
allPages: TQueryFnData[],
firstPageParam: TPageParam,
allPageParams: TPageParam[]
) => TPageParam | null | undefined;
/** 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);
/** Function to transform query data */
select?: (data: InfiniteData<TQueryFnData, TPageParam>) => TData;
/** Initial data to use while loading */
initialData?: InfiniteData<TQueryFnData, TPageParam> | (() => InfiniteData<TQueryFnData, TPageParam>);
/** Placeholder data to show while loading */
placeholderData?: InfiniteData<TQueryFnData, TPageParam> | ((previousData: InfiniteData<TQueryFnData, TPageParam> | undefined) => InfiniteData<TQueryFnData, TPageParam>);
/** Maximum number of pages to store */
maxPages?: number;
/** Whether to use infinite queries structure sharing */
structuralSharing?: boolean | ((oldData: InfiniteData<TQueryFnData, TPageParam> | undefined, newData: InfiniteData<TQueryFnData, TPageParam>) => InfiniteData<TQueryFnData, TPageParam>);
}Signal-based result object providing reactive access to infinite query state and page management.
interface CreateInfiniteQueryResult<TData, TError> {
/** Signal containing all pages of data */
data: Signal<InfiniteData<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 there is a next page available */
hasNextPage: Signal<boolean>;
/** Signal indicating if there is a previous page available */
hasPreviousPage: Signal<boolean>;
/** Signal indicating if currently fetching next page */
isFetchingNextPage: Signal<boolean>;
/** Signal indicating if currently fetching previous page */
isFetchingPreviousPage: Signal<boolean>;
/** Signal containing query status */
status: Signal<'pending' | 'error' | 'success'>;
/** Signal containing fetch status */
fetchStatus: Signal<'fetching' | 'paused' | 'idle'>;
/** Signal containing current failure count */
failureCount: Signal<number>;
/** Signal containing failure reason */
failureReason: Signal<TError | null>;
/** Function to fetch the next page */
fetchNextPage: (options?: { cancelRefetch?: boolean }) => Promise<InfiniteQueryObserverResult<TData, TError>>;
/** Function to fetch the previous page */
fetchPreviousPage: (options?: { cancelRefetch?: boolean }) => Promise<InfiniteQueryObserverResult<TData, TError>>;
// Type narrowing methods
isSuccess(this: CreateInfiniteQueryResult<TData, TError>): this is CreateInfiniteQueryResult<TData, TError>;
isError(this: CreateInfiniteQueryResult<TData, TError>): this is CreateInfiniteQueryResult<TData, TError>;
isPending(this: CreateInfiniteQueryResult<TData, TError>): this is CreateInfiniteQueryResult<TData, TError>;
}
interface DefinedCreateInfiniteQueryResult<TData, TError> extends CreateInfiniteQueryResult<TData, TError> {
/** Signal containing all pages of data (guaranteed to be defined) */
data: Signal<InfiniteData<TData>>;
}Type definition for the structure that holds infinite query data.
interface InfiniteData<TData, TPageParam = unknown> {
/** Array of all loaded pages */
pages: TData[];
/** Array of page parameters corresponding to each page */
pageParams: TPageParam[];
}Type definition for infinite query functions.
type InfiniteQueryFunction<TQueryFnData, TQueryKey, TPageParam> = (
context: {
queryKey: TQueryKey;
pageParam: TPageParam;
direction: 'forward' | 'backward';
meta: Record<string, unknown> | undefined;
signal: AbortSignal;
}
) => Promise<TQueryFnData>;Configuration interface for injectInfiniteQuery behavior.
interface InjectInfiniteQueryOptions {
/**
* The Injector in which to create the infinite query.
* If not provided, the current injection context will be used instead (via inject).
*/
injector?: Injector;
}Type definitions for different initial data scenarios.
type UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> = CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
initialData?: undefined | NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>> | InitialDataFunction<NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>>;
};
type DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> = CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
initialData: NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>> | (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>);
};@Component({
selector: 'app-infinite-scroll',
template: `
<div
class="infinite-scroll-container"
(scroll)="onScroll($event)"
>
<div *ngFor="let page of feedQuery.data()?.pages">
<div *ngFor="let item of page.items" class="feed-item">
{{ item.title }}
</div>
</div>
<div *ngIf="feedQuery.isFetchingNextPage()" class="loading">
Loading more...
</div>
</div>
`
})
export class InfiniteScrollComponent {
#http = inject(HttpClient);
feedQuery = injectInfiniteQuery(() => ({
queryKey: ['feed'],
queryFn: ({ pageParam = 0 }) =>
this.#http.get<FeedPage>(`/api/feed?offset=${pageParam}&limit=20`),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? lastPage.nextOffset : null;
}
}));
onScroll(event: Event) {
const element = event.target as HTMLElement;
const threshold = 200; // pixels from bottom
if (element.scrollTop + element.clientHeight >= element.scrollHeight - threshold) {
if (this.feedQuery.hasNextPage() && !this.feedQuery.isFetchingNextPage()) {
this.feedQuery.fetchNextPage();
}
}
}
}@Component({})
export class BidirectionalPaginationComponent {
#http = inject(HttpClient);
commentsQuery = injectInfiniteQuery(() => ({
queryKey: ['comments', 'post-123'],
queryFn: ({ pageParam = { cursor: null, direction: 'forward' } }) => {
const params = new URLSearchParams();
if (pageParam.cursor) {
params.set('cursor', pageParam.cursor);
}
params.set('direction', pageParam.direction);
return this.#http.get<CommentsPage>(`/api/comments?${params}`);
},
initialPageParam: { cursor: null, direction: 'forward' } as const,
getNextPageParam: (lastPage) => {
return lastPage.nextCursor
? { cursor: lastPage.nextCursor, direction: 'forward' as const }
: null;
},
getPreviousPageParam: (firstPage) => {
return firstPage.prevCursor
? { cursor: firstPage.prevCursor, direction: 'backward' as const }
: null;
}
}));
loadNewer() {
if (this.commentsQuery.hasPreviousPage()) {
this.commentsQuery.fetchPreviousPage();
}
}
loadOlder() {
if (this.commentsQuery.hasNextPage()) {
this.commentsQuery.fetchNextPage();
}
}
}@Component({})
export class InfiniteSearchComponent {
#http = inject(HttpClient);
searchTerm = signal('');
searchQuery = injectInfiniteQuery(() => ({
queryKey: ['search', this.searchTerm()],
queryFn: ({ pageParam = 1 }) => {
const term = this.searchTerm();
return this.#http.get<SearchResults>(`/api/search?q=${term}&page=${pageParam}`);
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : null;
},
enabled: () => this.searchTerm().length > 2,
staleTime: 5 * 60 * 1000 // 5 minutes
}));
// Get flat array of all results across pages
get allResults() {
return this.searchQuery.data()?.pages.flatMap(page => page.results) ?? [];
}
}@Component({})
export class MemoryOptimizedComponent {
#http = inject(HttpClient);
dataQuery = injectInfiniteQuery(() => ({
queryKey: ['large-dataset'],
queryFn: ({ pageParam = 1 }) =>
this.#http.get<DataPage>(`/api/data?page=${pageParam}`),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : null;
},
maxPages: 10, // Only keep last 10 pages in memory
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000 // 5 minutes
}));
}Install with Tessl CLI
npx tessl i tessl/npm-tanstack--angular-query-experimental