The framework agnostic core that powers TanStack Query for data fetching and caching
—
Specialized functionality for paginated data with automatic page management, bi-directional fetching, cursor-based pagination, and intelligent loading states.
Extended observer for managing paginated queries with automatic page handling.
/**
* Observer for infinite/paginated queries
* Extends QueryObserver with pagination-specific functionality
*/
class InfiniteQueryObserver<
TData = unknown,
TError = Error,
TQueryData = TData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown
> extends QueryObserver<InfiniteData<TData>, TError, InfiniteData<TQueryData>, TQueryKey> {
constructor(client: QueryClient, options: InfiniteQueryObserverOptions<TData, TError, TQueryData, TQueryKey, TPageParam>);
/**
* Fetch the next page of data
* @param options - Options for fetching next page
* @returns Promise resolving to updated infinite query result
*/
fetchNextPage(options?: FetchNextPageOptions): Promise<InfiniteQueryObserverResult<TData, TError>>;
/**
* Fetch the previous page of data
* @param options - Options for fetching previous page
* @returns Promise resolving to updated infinite query result
*/
fetchPreviousPage(options?: FetchPreviousPageOptions): Promise<InfiniteQueryObserverResult<TData, TError>>;
/**
* Get current infinite query result
* @returns Current infinite query observer result
*/
getCurrentResult(): InfiniteQueryObserverResult<TData, TError>;
}
interface InfiniteQueryObserverOptions<
TData = unknown,
TError = Error,
TQueryData = TData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown
> extends Omit<QueryObserverOptions<InfiniteData<TData>, TError, InfiniteData<TQueryData>, TQueryKey>, 'queryFn'> {
/**
* Query function that receives page parameter
* @param context - Query function context with pageParam
* @returns Promise resolving to page data
*/
queryFn: (context: QueryFunctionContext<TQueryKey, TPageParam>) => Promise<TQueryData>;
/**
* Function to get the next page parameter
* @param lastPage - The last page of data
* @param allPages - All pages fetched so far
* @param lastPageParam - The last page parameter used
* @param allPageParams - All page parameters used so far
* @returns Next page parameter or undefined if no more pages
*/
getNextPageParam: GetNextPageParamFunction<TQueryData, TPageParam>;
/**
* Function to get the previous page parameter
* @param firstPage - The first page of data
* @param allPages - All pages fetched so far
* @param firstPageParam - The first page parameter used
* @param allPageParams - All page parameters used so far
* @returns Previous page parameter or undefined if no more pages
*/
getPreviousPageParam?: GetPreviousPageParamFunction<TQueryData, TPageParam>;
/**
* Initial page parameter for the first page
*/
initialPageParam: TPageParam;
/**
* Maximum number of pages to keep in memory
* Older pages are removed when limit is exceeded
*/
maxPages?: number;
}
interface InfiniteQueryObserverResult<TData = unknown, TError = Error>
extends QueryObserverResult<InfiniteData<TData>, TError> {
/**
* Function to fetch the next page
* @param options - Fetch options
* @returns Promise resolving to updated result
*/
fetchNextPage: (options?: FetchNextPageOptions) => Promise<InfiniteQueryObserverResult<TData, TError>>;
/**
* Function to fetch the previous page
* @param options - Fetch options
* @returns Promise resolving to updated result
*/
fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise<InfiniteQueryObserverResult<TData, TError>>;
/** Whether there is a next page available */
hasNextPage: boolean;
/** Whether there is a previous page available */
hasPreviousPage: boolean;
/** Whether currently fetching the next page */
isFetchingNextPage: boolean;
/** Whether currently fetching the previous page */
isFetchingPreviousPage: boolean;
/** Whether next page fetch failed */
isFetchNextPageError: boolean;
/** Whether previous page fetch failed */
isFetchPreviousPageError: boolean;
}
type GetNextPageParamFunction<TQueryData = unknown, TPageParam = unknown> = (
lastPage: TQueryData,
allPages: TQueryData[],
lastPageParam: TPageParam,
allPageParams: TPageParam[]
) => TPageParam | undefined | null;
type GetPreviousPageParamFunction<TQueryData = unknown, TPageParam = unknown> = (
firstPage: TQueryData,
allPages: TQueryData[],
firstPageParam: TPageParam,
allPageParams: TPageParam[]
) => TPageParam | undefined | null;
interface FetchNextPageOptions {
throwOnError?: boolean;
cancelRefetch?: boolean;
}
interface FetchPreviousPageOptions {
throwOnError?: boolean;
cancelRefetch?: boolean;
}Usage Examples:
import { QueryClient, InfiniteQueryObserver } from "@tanstack/query-core";
const queryClient = new QueryClient();
// Cursor-based pagination
const infiniteObserver = new InfiniteQueryObserver(queryClient, {
queryKey: ['posts'],
queryFn: async ({ pageParam = null }) => {
const url = pageParam
? `/api/posts?cursor=${pageParam}`
: '/api/posts';
const response = await fetch(url);
return response.json();
},
initialPageParam: null,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined,
});
// Subscribe to changes
const unsubscribe = infiniteObserver.subscribe((result) => {
console.log('All pages:', result.data?.pages);
console.log('Page params:', result.data?.pageParams);
console.log('Has next page:', result.hasNextPage);
console.log('Is fetching next:', result.isFetchingNextPage);
// Flatten all posts from all pages
const allPosts = result.data?.pages.flatMap(page => page.posts) ?? [];
console.log('Total posts:', allPosts.length);
});
// Load next page
const nextPageResult = await infiniteObserver.fetchNextPage();
// Load previous page
const prevPageResult = await infiniteObserver.fetchPreviousPage();
// Cleanup
unsubscribe();
infiniteObserver.destroy();Direct methods on QueryClient for infinite queries.
/**
* Fetch an infinite query imperatively
* @param options - Infinite query options
* @returns Promise resolving to infinite data
*/
fetchInfiniteQuery<T>(options: FetchInfiniteQueryOptions<T>): Promise<InfiniteData<T>>;
/**
* Prefetch an infinite query
* @param options - Infinite query options
* @returns Promise that resolves when prefetch completes
*/
prefetchInfiniteQuery<T>(options: FetchInfiniteQueryOptions<T>): Promise<void>;
/**
* Ensure infinite query data exists
* @param options - Infinite query options with revalidation
* @returns Promise resolving to infinite data
*/
ensureInfiniteQueryData<T>(options: EnsureInfiniteQueryDataOptions<T>): Promise<InfiniteData<T>>;
interface FetchInfiniteQueryOptions<T> {
queryKey: QueryKey;
queryFn: QueryFunction<T>;
initialPageParam: unknown;
getNextPageParam: GetNextPageParamFunction<T>;
getPreviousPageParam?: GetPreviousPageParamFunction<T>;
pages?: number;
staleTime?: number;
gcTime?: number;
retry?: RetryValue<T>;
networkMode?: NetworkMode;
meta?: QueryMeta;
signal?: AbortSignal;
}
interface EnsureInfiniteQueryDataOptions<T> extends FetchInfiniteQueryOptions<T> {
revalidateIfStale?: boolean;
}Usage Examples:
// Fetch infinite query imperatively
const infiniteData = await queryClient.fetchInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?page=${pageParam}`);
return response.json();
},
initialPageParam: 0,
getNextPageParam: (lastPage, pages) =>
lastPage.hasMore ? pages.length : undefined,
pages: 3, // Load first 3 pages
});
// Prefetch infinite query
await queryClient.prefetchInfiniteQuery({
queryKey: ['popular-posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts/popular?page=${pageParam}`);
return response.json();
},
initialPageParam: 0,
getNextPageParam: (lastPage, pages) =>
lastPage.hasMore ? pages.length : undefined,
pages: 2, // Prefetch first 2 pages
});The data structure returned by infinite queries.
interface InfiniteData<TData> {
/**
* Array of all pages that have been fetched
* Each page contains the data returned by queryFn for that page
*/
pages: TData[];
/**
* Array of all page parameters that were used to fetch each page
* Corresponds to the pages array - pageParams[i] was used to fetch pages[i]
*/
pageParams: unknown[];
}Usage Examples:
// Working with infinite data
const result = infiniteObserver.getCurrentResult();
if (result.data) {
// Access all pages
console.log('Number of pages:', result.data.pages.length);
// Access specific page
const firstPage = result.data.pages[0];
const lastPage = result.data.pages[result.data.pages.length - 1];
// Access page parameters
console.log('Page params:', result.data.pageParams);
// Flatten all data from all pages
const allItems = result.data.pages.flatMap(page =>
Array.isArray(page) ? page : page.items || []
);
console.log('Total items across all pages:', allItems.length);
}Common pagination patterns and how to implement them.
// Offset-based pagination
const offsetPagination = new InfiniteQueryObserver(queryClient, {
queryKey: ['posts', 'offset'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?offset=${pageParam}&limit=20`);
return response.json();
},
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.items.length < 20) return undefined; // No more pages
return allPages.length * 20; // Next offset
},
});
// Cursor-based pagination
const cursorPagination = new InfiniteQueryObserver(queryClient, {
queryKey: ['posts', 'cursor'],
queryFn: async ({ pageParam }) => {
const url = pageParam
? `/api/posts?cursor=${pageParam}&limit=20`
: '/api/posts?limit=20';
const response = await fetch(url);
return response.json();
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
});
// Page number pagination
const pageNumberPagination = new InfiniteQueryObserver(queryClient, {
queryKey: ['posts', 'pages'],
queryFn: async ({ pageParam = 1 }) => {
const response = await fetch(`/api/posts?page=${pageParam}&limit=20`);
return response.json();
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.totalPages > allPages.length
? allPages.length + 1
: undefined;
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
return firstPageParam > 1 ? firstPageParam - 1 : undefined;
},
});Advanced configuration options specific to infinite queries.
interface InfiniteQueryObserverOptions<T> {
/**
* Maximum number of pages to keep in memory
* When exceeded, oldest pages are removed
* Useful for managing memory usage in long-running infinite queries
*/
maxPages?: number;
/**
* Function to determine if there are more pages to fetch forward
* Called with the last page and all pages
*/
getNextPageParam: GetNextPageParamFunction<T>;
/**
* Function to determine if there are more pages to fetch backward
* Called with the first page and all pages
*/
getPreviousPageParam?: GetPreviousPageParamFunction<T>;
/**
* Initial page parameter for the first page
* This is passed to queryFn as pageParam for the first fetch
*/
initialPageParam: unknown;
/**
* Custom select function for infinite queries
* Can transform the InfiniteData structure
*/
select?: (data: InfiniteData<T>) => InfiniteData<T>;
}interface QueryFunctionContext<TQueryKey extends QueryKey = QueryKey, TPageParam = unknown> {
queryKey: TQueryKey;
signal: AbortSignal;
meta: QueryMeta | undefined;
pageParam: TPageParam;
direction: 'forward' | 'backward';
}
type RetryValue<TError> = boolean | number | ((failureCount: number, error: TError) => boolean);
type NetworkMode = 'online' | 'always' | 'offlineFirst';
interface QueryMeta extends Record<string, unknown> {}Install with Tessl CLI
npx tessl i tessl/npm-tanstack--query-core