React Hooks library for remote data fetching with stale-while-revalidate caching strategy
—
The useSWRInfinite hook provides pagination and infinite loading capabilities with automatic page management, size control, and optimized revalidation.
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);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;
}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];
};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)
});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();
};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 }
);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