CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--query-core

The framework agnostic core that powers TanStack Query for data fetching and caching

Pending
Overview
Eval results
Files

infinite-queries.mddocs/

Infinite Queries

Specialized functionality for paginated data with automatic page management, bi-directional fetching, cursor-based pagination, and intelligent loading states.

Capabilities

InfiniteQueryObserver

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();

Imperative Infinite Query Methods

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
});

InfiniteData Structure

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);
}

Pagination Patterns

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;
  },
});

Infinite Query Configuration

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>;
}

Core Types

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

docs

browser-integration.md

cache-management.md

client-management.md

hydration.md

index.md

infinite-queries.md

mutations.md

query-observers.md

query-operations.md

utilities.md

tile.json