CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-swr

React Hooks library for remote data fetching with stale-while-revalidate caching strategy

Pending
Overview
Eval results
Files

infinite-loading.mddocs/

Infinite Loading

The useSWRInfinite hook provides pagination and infinite loading capabilities with automatic page management, size control, and optimized revalidation.

Capabilities

useSWRInfinite Hook

Hook for infinite loading and pagination scenarios with automatic page management.

/**
 * Hook for infinite loading and pagination scenarios
 * @param getKey - Function that returns the key for each page
 * @param fetcher - Function that fetches data for each page
 * @param config - Configuration options extending SWRConfiguration
 * @returns SWRInfiniteResponse with data array, size control, and loading states
 */
function useSWRInfinite<Data = any, Error = any>(
  getKey: SWRInfiniteKeyLoader<Data>,
  fetcher?: SWRInfiniteFetcher<Data> | null,
  config?: SWRInfiniteConfiguration<Data, Error>
): SWRInfiniteResponse<Data, Error>;

function unstable_serialize(getKey: SWRInfiniteKeyLoader): string | undefined;

Usage Examples:

import useSWRInfinite from "swr/infinite";

// Basic infinite loading
const getKey = (pageIndex: number, previousPageData: any) => {
  if (previousPageData && !previousPageData.length) return null; // reached the end
  return `/api/issues?page=${pageIndex}&limit=10`;
};

const { data, error, size, setSize, isValidating } = useSWRInfinite(
  getKey,
  fetcher
);

// Cursor-based pagination
const getKey = (pageIndex: number, previousPageData: any) => {
  if (previousPageData && !previousPageData.nextCursor) return null;
  if (pageIndex === 0) return "/api/posts?limit=10";
  return `/api/posts?cursor=${previousPageData.nextCursor}&limit=10`;
};

// Load more functionality
const issues = data ? data.flat() : [];
const isLoadingMore = data && typeof data[size - 1] === "undefined";
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);

const loadMore = () => setSize(size + 1);

SWR Infinite Response

The return value from useSWRInfinite with specialized properties for pagination.

interface SWRInfiniteResponse<Data, Error> {
  /** Array of page data (undefined if not loaded) */
  data: Data[] | undefined;
  /** Error from any page (undefined if no error) */
  error: Error | undefined;
  /** Scoped mutate function for infinite data */
  mutate: SWRInfiniteKeyedMutator<Data>;
  /** Current number of pages */
  size: number;
  /** Function to change the number of pages */
  setSize: (size: number | ((size: number) => number)) => Promise<Data[] | undefined>;
  /** True when any page is validating */
  isValidating: boolean;
  /** True when loading the first page for the first time */
  isLoading: boolean;
}

Key Loader Function

Function that generates the key for each page, enabling different pagination strategies.

type SWRInfiniteKeyLoader<Data = any, Args = any> = (
  index: number,
  previousPageData: Data | null
) => Args | null;

Key Loader Examples:

// Offset-based pagination
const getKey = (pageIndex: number, previousPageData: any) => {
  return `/api/data?offset=${pageIndex * 10}&limit=10`;
};

// Cursor-based pagination
const getKey = (pageIndex: number, previousPageData: any) => {
  if (previousPageData && !previousPageData.nextCursor) return null;
  if (pageIndex === 0) return "/api/data?limit=10";
  return `/api/data?cursor=${previousPageData.nextCursor}&limit=10`;
};

// Page number pagination
const getKey = (pageIndex: number, previousPageData: any) => {
  if (previousPageData && previousPageData.isLastPage) return null;
  return `/api/data?page=${pageIndex + 1}`;
};

// Conditional loading (stop when empty)
const getKey = (pageIndex: number, previousPageData: any) => {
  if (previousPageData && !previousPageData.length) return null;
  return `/api/data?page=${pageIndex}`;
};

// Complex key with multiple parameters
const getKey = (pageIndex: number, previousPageData: any) => {
  return ["/api/search", query, pageIndex, filters];
};

Configuration Options

Extended configuration options specific to infinite loading.

interface SWRInfiniteConfiguration<Data = any, Error = any> 
  extends Omit<SWRConfiguration<Data[], Error>, 'fetcher'> {
  /** Initial number of pages to load (default: 1) */
  initialSize?: number;
  /** Revalidate all pages when any page revalidates (default: false) */
  revalidateAll?: boolean;
  /** Keep page size when key changes (default: false) */
  persistSize?: boolean;
  /** Revalidate first page on mount and focus (default: true) */
  revalidateFirstPage?: boolean;
  /** Load pages in parallel instead of sequentially (default: false) */
  parallel?: boolean;
  /** Custom comparison function for page data */
  compare?: SWRInfiniteCompareFn<Data>;
  /** Fetcher function for infinite data */
  fetcher?: SWRInfiniteFetcher<Data> | null;
}

type SWRInfiniteFetcher<Data = any, KeyLoader extends SWRInfiniteKeyLoader<any> = SWRInfiniteKeyLoader<Data>> = 
  KeyLoader extends (...args: any[]) => infer Arg | null 
    ? Arg extends null 
      ? never 
      : (...args: [Arg]) => Data | Promise<Data>
    : never;

interface SWRInfiniteCompareFn<Data> {
  (a: Data | undefined, b: Data | undefined): boolean;
}

Configuration Examples:

// Load 3 pages initially
const { data } = useSWRInfinite(getKey, fetcher, {
  initialSize: 3
});

// Parallel page loading
const { data } = useSWRInfinite(getKey, fetcher, {
  parallel: true
});

// Revalidate all pages on focus
const { data } = useSWRInfinite(getKey, fetcher, {
  revalidateAll: true,
  revalidateOnFocus: true
});

// Persist page size across key changes
const { data } = useSWRInfinite(getKey, fetcher, {
  persistSize: true
});

// Custom page comparison
const { data } = useSWRInfinite(getKey, fetcher, {
  compare: (a, b) => JSON.stringify(a) === JSON.stringify(b)
});

Size Management

Control the number of pages loaded with the setSize function.

setSize: (size: number | ((size: number) => number)) => Promise<Data[] | undefined>;

Size Management Examples:

const { data, size, setSize } = useSWRInfinite(getKey, fetcher);

// Load more pages
const loadMore = () => setSize(size + 1);

// Load specific number of pages
const loadExact = (pageCount: number) => setSize(pageCount);

// Function-based size update
const doublePages = () => setSize(current => current * 2);

// Reset to first page
const reset = () => setSize(1);

// Load all available pages (be careful!)
const loadAll = () => {
  // Keep loading until no more data
  let currentSize = size;
  const loadNext = () => {
    setSize(currentSize + 1).then((newData) => {
      if (newData && newData[currentSize]?.length > 0) {
        currentSize++;
        loadNext();
      }
    });
  };
  loadNext();
};

Infinite Mutate Function

Specialized mutate function for infinite data structures.

interface SWRInfiniteKeyedMutator<Data> {
  /**
   * Mutate infinite data with support for page-level updates
   * @param data - New data array, Promise, or function returning new data
   * @param options - Mutation options including revalidation control
   * @returns Promise resolving to the new data array
   */
  (
    data?: Data[] | Promise<Data[]> | ((currentData: Data[] | undefined) => Data[] | undefined),
    options?: boolean | SWRInfiniteMutatorOptions<Data>
  ): Promise<Data[] | undefined>;
}

interface SWRInfiniteMutatorOptions<Data = any> {
  /** Whether to revalidate after mutation (default: true) */
  revalidate?: boolean;
  /** Control which pages to revalidate */
  revalidate?: boolean | ((pageData: Data, pageArg: any) => boolean);
}

Infinite Mutate Examples:

const { data, mutate } = useSWRInfinite(getKey, fetcher);

// Update all pages
await mutate([...newPagesData]);

// Add new item to first page
await mutate((currentData) => {
  if (!currentData) return undefined;
  return [
    [newItem, ...currentData[0]],
    ...currentData.slice(1)
  ];
});

// Remove item from all pages
await mutate((currentData) => {
  if (!currentData) return undefined;
  return currentData.map(page => 
    page.filter(item => item.id !== removedItemId)
  );
});

// Optimistic update for new page
await mutate(
  [...(data || []), optimisticNewPage],
  { revalidate: false }
);

Advanced Patterns

Common patterns for complex infinite loading scenarios.

Load More Button:

function InfiniteList() {
  const { data, error, size, setSize, isValidating } = useSWRInfinite(
    getKey,
    fetcher
  );

  const issues = data ? data.flat() : [];
  const isLoadingMore = data && typeof data[size - 1] === "undefined";
  const isEmpty = data?.[0]?.length === 0;
  const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);

  return (
    <div>
      {issues.map(issue => (
        <div key={issue.id}>{issue.title}</div>
      ))}
      
      {error && <div>Error: {error.message}</div>}
      
      <button
        disabled={isLoadingMore || isReachingEnd}
        onClick={() => setSize(size + 1)}
      >
        {isLoadingMore ? "Loading..." : 
         isReachingEnd ? "No more data" : "Load more"}
      </button>
    </div>
  );
}

Infinite Scroll:

function InfiniteScroll() {
  const { data, setSize, size } = useSWRInfinite(getKey, fetcher);
  const [isIntersecting, setIsIntersecting] = useState(false);
  const loadMoreRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => setIsIntersecting(entry.isIntersecting),
      { threshold: 1 }
    );
    
    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }
    
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    if (isIntersecting) {
      setSize(size + 1);
    }
  }, [isIntersecting, setSize, size]);

  const items = data ? data.flat() : [];

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>{item.title}</div>
      ))}
      <div ref={loadMoreRef} />
    </div>
  );
}

Search with Infinite Results:

function InfiniteSearch() {
  const [query, setQuery] = useState("");
  
  const getKey = (pageIndex: number, previousPageData: any) => {
    if (!query) return null;
    if (previousPageData && !previousPageData.length) return null;
    return `/api/search?q=${query}&page=${pageIndex}`;
  };

  const { data, setSize, size } = useSWRInfinite(getKey, fetcher);

  // Reset pages when query changes
  useEffect(() => {
    setSize(1);
  }, [query, setSize]);

  const results = data ? data.flat() : [];

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      
      {results.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
      
      {query && (
        <button onClick={() => setSize(size + 1)}>
          Load more results
        </button>
      )}
    </div>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-swr

docs

cache-management.md

core-data-fetching.md

global-configuration.md

immutable-data.md

index.md

infinite-loading.md

mutations.md

subscriptions.md

tile.json