CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-query

Hooks for managing, caching and syncing asynchronous and remote data in React

Pending
Overview
Eval results
Files

infinite-queries.mddocs/

Infinite Queries

Specialized query hook for paginated data with infinite scrolling support, automatic page management, and cursor-based pagination. The useInfiniteQuery hook is perfect for implementing "Load More" buttons or infinite scroll functionality.

Capabilities

useInfiniteQuery Hook

The main hook for fetching paginated data with automatic pagination management.

/**
 * Fetch paginated data with infinite loading support
 * @param options - Infinite query configuration options
 * @returns Infinite query result with pagination utilities
 */
function useInfiniteQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
  options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>
): UseInfiniteQueryResult<TData, TError>;

/**
 * Fetch paginated data using separate queryKey and queryFn parameters
 * @param queryKey - Unique identifier for the query
 * @param queryFn - Function that returns paginated data
 * @param options - Additional infinite query configuration
 * @returns Infinite query result with pagination utilities
 */
function useInfiniteQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
  queryKey: TQueryKey,
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
  options?: Omit<UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, 'queryKey' | 'queryFn'>
): UseInfiniteQueryResult<TData, TError>;

Usage Examples:

import { useInfiniteQuery } from "react-query";

// Basic infinite query with cursor-based pagination
const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  isLoading
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 0 }) =>
    fetch(`/api/posts?cursor=${pageParam}&limit=10`)
      .then(res => res.json()),
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined
});

// Infinite query with page-based pagination
const {
  data: searchResults,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage
} = useInfiniteQuery({
  queryKey: ['search', searchTerm],
  queryFn: ({ pageParam = 1 }) =>
    fetch(`/api/search?q=${searchTerm}&page=${pageParam}&limit=20`)
      .then(res => res.json()),
  getNextPageParam: (lastPage, allPages) => {
    return lastPage.hasMore ? allPages.length + 1 : undefined;
  },
  enabled: !!searchTerm
});

// Using the paginated data
const allPosts = data?.pages.flatMap(page => page.posts) ?? [];

return (
  <div>
    {allPosts.map(post => (
      <div key={post.id}>{post.title}</div>
    ))}
    
    {hasNextPage && (
      <button 
        onClick={() => fetchNextPage()}
        disabled={isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading more...' : 'Load More'}
      </button>
    )}
  </div>
);

Infinite Query Result Interface

The return value from useInfiniteQuery containing paginated data and navigation utilities.

interface UseInfiniteQueryResult<TData = unknown, TError = unknown> {
  /** Paginated data structure with pages and page parameters */
  data: InfiniteData<TData> | undefined;
  /** Error object if the query failed */
  error: TError | null;
  /** Function to fetch the next page */
  fetchNextPage: (options?: FetchNextPageOptions) => Promise<UseInfiniteQueryResult<TData, TError>>;
  /** Function to fetch the previous page */
  fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise<UseInfiniteQueryResult<TData, TError>>;
  /** True if there are more pages to fetch forward */
  hasNextPage: boolean;
  /** True if there are more pages to fetch backward */
  hasPreviousPage: boolean;
  /** True if currently fetching next page */
  isFetchingNextPage: boolean;
  /** True if currently fetching previous page */
  isFetchingPreviousPage: boolean;
  /** True if this is the first time the query is loading */
  isLoading: boolean;
  /** True if the query is pending (loading or paused) */
  isPending: boolean;
  /** True if the query succeeded and has data */
  isSuccess: boolean;
  /** True if the query is in an error state */
  isError: boolean;
  /** True if the query is currently fetching any page */
  isFetching: boolean;
  /** True if the query is refetching in the background */
  isRefetching: boolean;
  /** Current status of the query */
  status: 'pending' | 'error' | 'success';
  /** Current fetch status */
  fetchStatus: 'fetching' | 'paused' | 'idle';
  /** Function to manually refetch all pages */
  refetch: () => Promise<UseInfiniteQueryResult<TData, TError>>;
  /** Function to remove the query from cache */
  remove: () => void;
  /** True if query data is stale */
  isStale: boolean;
  /** True if data exists in cache */
  isPlaceholderData: boolean;
  /** True if using previous data during refetch */
  isPreviousData: boolean;
  /** Timestamp when data was last updated */
  dataUpdatedAt: number;
  /** Timestamp when error occurred */
  errorUpdatedAt: number;
  /** Number of times query has failed */
  failureCount: number;
  /** Reason query is paused */
  failureReason: TError | null;
  /** True if currently paused */
  isPaused: boolean;
}

interface InfiniteData<TData> {
  /** Array of page data */
  pages: TData[];
  /** Array of page parameters used to fetch each page */
  pageParams: unknown[];
}

interface FetchNextPageOptions {
  /** Cancel ongoing requests before fetching */
  cancelRefetch?: boolean;
}

interface FetchPreviousPageOptions {
  /** Cancel ongoing requests before fetching */
  cancelRefetch?: boolean;
}

Infinite Query Options Interface

Configuration options for useInfiniteQuery hook.

interface UseInfiniteQueryOptions<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey> {
  /** Unique identifier for the query */
  queryKey?: TQueryKey;
  /** Function that returns paginated data */
  queryFn?: QueryFunction<TQueryFnData, TQueryKey>;
  /** Function to get the next page parameter */
  getNextPageParam: GetNextPageParamFunction<TQueryFnData>;
  /** Function to get the previous page parameter */
  getPreviousPageParam?: GetPreviousPageParamFunction<TQueryFnData>;
  /** Whether the query should run automatically */
  enabled?: boolean;
  /** Retry configuration */
  retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
  /** Delay between retries */
  retryDelay?: number | ((retryAttempt: number, error: TError) => number);
  /** Time before data is considered stale */
  staleTime?: number;
  /** Time before inactive queries are garbage collected */
  cacheTime?: number;
  /** Interval for automatic refetching */
  refetchInterval?: number | false;
  /** Whether to refetch when window is not focused */
  refetchIntervalInBackground?: boolean;
  /** When to refetch on component mount */
  refetchOnMount?: boolean | "always";
  /** When to refetch on window focus */
  refetchOnWindowFocus?: boolean | "always";
  /** When to refetch on network reconnection */
  refetchOnReconnect?: boolean | "always";
  /** Transform or select data subset */
  select?: (data: InfiniteData<TQueryFnData>) => TData;
  /** Initial data to use before first fetch */
  initialData?: InfiniteData<TQueryFnData> | (() => InfiniteData<TQueryFnData>);
  /** Placeholder data while loading */
  placeholderData?: InfiniteData<TQueryFnData> | (() => InfiniteData<TQueryFnData>);
  /** Callback on successful query */
  onSuccess?: (data: InfiniteData<TQueryFnData>) => void;
  /** Callback on query error */
  onError?: (error: TError) => void;
  /** Callback after query settles (success or error) */
  onSettled?: (data: InfiniteData<TQueryFnData> | undefined, error: TError | null) => void;
  /** React context for QueryClient */
  context?: React.Context<QueryClient | undefined>;
  /** Whether to throw errors to error boundaries */
  useErrorBoundary?: boolean | ((error: TError) => boolean);
  /** Whether to suspend component rendering */
  suspense?: boolean;
  /** Whether to keep previous data during refetch */
  keepPreviousData?: boolean;
  /** Additional metadata */
  meta?: QueryMeta;
  /** Maximum number of pages to keep in memory */
  maxPages?: number;
}

Page Parameter Functions

Functions that determine pagination parameters.

type GetNextPageParamFunction<TQueryFnData = unknown> = (
  lastPage: TQueryFnData,
  allPages: TQueryFnData[]
) => unknown;

type GetPreviousPageParamFunction<TQueryFnData = unknown> = (
  firstPage: TQueryFnData,
  allPages: TQueryFnData[]
) => unknown;

Advanced Usage Patterns

Infinite Scroll Implementation

Automatic loading when user scrolls near the bottom:

import { useInfiniteQuery } from "react-query";
import { useIntersectionObserver } from "./hooks/useIntersectionObserver";

function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 0 }) =>
      fetch(`/api/posts?cursor=${pageParam}&limit=10`)
        .then(res => res.json()),
    getNextPageParam: (lastPage) => lastPage.nextCursor
  });

  // Intersection observer to trigger loading
  const { ref, entry } = useIntersectionObserver({
    threshold: 0.1,
    rootMargin: '100px'
  });

  // Fetch next page when sentinel comes into view
  React.useEffect(() => {
    if (entry?.isIntersecting && hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  }, [entry, fetchNextPage, hasNextPage, isFetchingNextPage]);

  if (isLoading) return <div>Loading...</div>;

  const allPosts = data?.pages.flatMap(page => page.posts) ?? [];

  return (
    <div>
      {allPosts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      
      {/* Sentinel element for intersection observer */}
      <div ref={ref} style={{ height: '20px' }}>
        {isFetchingNextPage && <div>Loading more posts...</div>}
      </div>
      
      {!hasNextPage && (
        <div>No more posts to load</div>
      )}
    </div>
  );
}

Bidirectional Infinite Loading

Supporting both forward and backward pagination:

function ChatMessages({ channelId }: { channelId: string }) {
  const {
    data,
    fetchNextPage,
    fetchPreviousPage,
    hasNextPage,
    hasPreviousPage,
    isFetchingNextPage,
    isFetchingPreviousPage
  } = useInfiniteQuery({
    queryKey: ['messages', channelId],
    queryFn: ({ pageParam }) => {
      const { cursor, direction = 'newer' } = pageParam || {};
      return fetch(`/api/channels/${channelId}/messages?cursor=${cursor}&direction=${direction}&limit=50`)
        .then(res => res.json());
    },
    getNextPageParam: (lastPage) => 
      lastPage.hasNewer ? { cursor: lastPage.newestCursor, direction: 'newer' } : undefined,
    getPreviousPageParam: (firstPage) =>
      firstPage.hasOlder ? { cursor: firstPage.oldestCursor, direction: 'older' } : undefined,
    select: (data) => ({
      pages: [...data.pages].reverse(), // Show oldest messages first
      pageParams: data.pageParams
    })
  });

  const allMessages = data?.pages.flatMap(page => page.messages) ?? [];

  return (
    <div className="chat-container">
      {hasPreviousPage && (
        <button 
          onClick={() => fetchPreviousPage()}
          disabled={isFetchingPreviousPage}
        >
          {isFetchingPreviousPage ? 'Loading older...' : 'Load Older Messages'}
        </button>
      )}
      
      {allMessages.map(message => (
        <MessageComponent key={message.id} message={message} />
      ))}
      
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading newer...' : 'Load Newer Messages'}
        </button>
      )}
    </div>
  );
}

Search with Infinite Results

Combining search functionality with infinite loading:

function InfiniteSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedTerm, setDebouncedTerm] = useState('');

  // Debounce search term
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedTerm(searchTerm), 300);
    return () => clearTimeout(timer);
  }, [searchTerm]);

  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    refetch
  } = useInfiniteQuery({
    queryKey: ['search', debouncedTerm],
    queryFn: ({ pageParam = 1 }) =>
      fetch(`/api/search?q=${debouncedTerm}&page=${pageParam}&limit=20`)
        .then(res => res.json()),
    getNextPageParam: (lastPage, allPages) =>
      lastPage.hasMore ? allPages.length + 1 : undefined,
    enabled: debouncedTerm.length > 2,
    keepPreviousData: true // Keep showing old results while new search loads
  });

  const allResults = data?.pages.flatMap(page => page.results) ?? [];

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      
      {isLoading && <div>Searching...</div>}
      
      {allResults.length > 0 && (
        <div>
          <p>{data.pages[0].totalCount} results found</p>
          {allResults.map(result => (
            <SearchResult key={result.id} result={result} />
          ))}
          
          {hasNextPage && (
            <button 
              onClick={() => fetchNextPage()}
              disabled={isFetchingNextPage}
            >
              {isFetchingNextPage ? 'Loading more...' : 'Load More Results'}
            </button>
          )}
        </div>
      )}
      
      {debouncedTerm.length > 2 && allResults.length === 0 && !isLoading && (
        <div>No results found for "{debouncedTerm}"</div>
      )}
    </div>
  );
}

Performance Optimization

Limiting pages in memory to prevent memory leaks:

const { data } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  maxPages: 10, // Only keep 10 pages in memory
  select: (data) => ({
    pages: data.pages,
    pageParams: data.pageParams
  })
});

// Or manually clean up old pages
const { data, fetchNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  onSuccess: (data) => {
    // Keep only last 5 pages
    if (data.pages.length > 5) {
      const newData = {
        pages: data.pages.slice(-5),
        pageParams: data.pageParams.slice(-5)
      };
      queryClient.setQueryData(['posts'], newData);
    }
  }
});

Install with Tessl CLI

npx tessl i tessl/npm-react-query

docs

context.md

error-handling.md

index.md

infinite-queries.md

mutations.md

parallel-queries.md

queries.md

ssr.md

status.md

tile.json