Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use useSWRInfinite para lidar com dados paginados e de scroll infinito. Defina uma função getKey que retorne a URL correta para cada índice de página e chame setSize para carregar mais páginas.
"use client";
import useSWRInfinite from "swr/infinite";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
const getKey = (pageIndex: number, previousPageData: any[] | null) => {
if (previousPageData && previousPageData.length === 0) return null; // chegou ao fim
return `/api/posts?page=${pageIndex}&limit=10`;
};
function PostFeed() {
const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(getKey, fetcher);
const posts = data ? data.flat() : [];
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === "undefined");
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);
return (
<div>
{posts.map((post) => (
<article key={post.id}>{post.title}</article>
))}
<button
disabled={isLoadingMore || isReachingEnd}
onClick={() => setSize(size + 1)}
>
{isLoadingMore ? "Carregando..." : isReachingEnd ? "Sem mais posts" : "Carregar mais"}
</button>
</div>
);
}"use client";
import useSWRInfinite from "swr/infinite";
interface Product {
id: string;
name: string;
price: number;
}
interface PageResponse {
items: Product[];
nextCursor: string | null;
}
const fetcher = (url: string): Promise<PageResponse> =>
fetch(url).then((r) => r.json());
export default function ProductCatalog() {
const { data, size, setSize, isLoading } = useSWRInfinite<PageResponse>(
(pageIndex, previousPageData) => {
// Primeira página - nenhum cursor necessário
if (pageIndex === 0) return "/api/products?limit=20";
// Não há mais páginas
if (!previousPageData?.nextCursor) return null;
// Paginação baseada em cursor
return `/api/products?cursor=${previousPageData.nextCursor}&limit=20`;
},
fetcher
);
const products = data?.flatMap((page) => page.items) ?? [];
const hasMore = data?.[data.length - 1]?.nextCursor !== null;
if (isLoading) return <div>Carregando produtos...</div>;
return (
<div>
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<div key={product.id} className="border p-4 rounded">
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
{hasMore && (
<button onClick={() => setSize(size + 1)}>Carregar Mais</button>
)}
</div>
);
}useSWRInfinite gerencia um array de páginas, onde cada página é o resultado de um fetch separado.getKey(pageIndex, previousPageData) é chamada para cada índice de página a partir de 0.null de getKey informa ao SWR para parar de buscar páginas adicionais.data é um array de arrays (ou array de objetos de página), uma entrada por página carregada.setSize(n) define quantas páginas devem ser carregadas. SWR busca quaisquer páginas que ainda não estejam em cache.Paginação baseada em offset:
const getKey = (pageIndex: number) => `/api/items?offset=${pageIndex * 20}&limit=20`;Com Intersection Observer para scroll infinito:
import { useRef, useCallback } from "react";
function InfiniteList() {
const { data, size, setSize } = useSWRInfinite(getKey, fetcher);
const observer = useRef<IntersectionObserver>();
const lastElementRef = useCallback(
(node: HTMLElement | null) => {
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setSize((s) => s + 1);
}
});
if (node) observer.current.observe(node);
},
[setSize]
);
const items = data?.flat() ?? [];
return (
<ul>
{items.map((item, i) => (
<li key={item.id} ref={i === items.length - 1 ? lastElementRef : undefined}>
{item.name}
</li>
))}
</ul>
);
}Fetch de páginas em paralelo:
const { data } = useSWRInfinite(getKey, fetcher, {
parallel: true, // Busca todas as páginas em paralelo na revalidação
});useSWRInfinite<Data, Error> tipam cada página no array de dados.data é Data[] | undefined - um array de páginas.getKey recebe (pageIndex: number, previousPageData: Data | null).const { data } = useSWRInfinite<Product[]>(getKey, fetcher);
// data: Product[][] | undefined
const allProducts = data?.flat() ?? [];
// allProducts: Product[]data é um array de páginas, não uma lista achatada. Você quase sempre precisará de .flat() ou .flatMap() para obter um único array.revalidateFirstPage: false pode prevenir re-fetches desnecessários da página 0 ao carregar páginas adicionais, mas significa que os dados da página 0 podem ficar desatualizados.setSize dispara fetches para quaisquer páginas ainda não em cache. Definir setSize(1) não descarta as páginas 2+ do cache; apenas controla o que é renderizado.| Abordagem | Prós | Contras |
|---|---|---|
| useSWRInfinite | Embutido no SWR, cache em nível de página | Lógica getKey complexa, formato de dados array-de-arrays |
| useSWR manual por página | Modelo mental mais simples | Sem "carregar mais" ou rastreamento de página embutidos |
| React Query useInfiniteQuery | API similar, getNextPageParam embutido | Ecossistema de biblioteca diferente |
| Server Components com searchParams | Sem JS no cliente, amigável para SEO | Recarregamento completo da página a cada mudança de página |
Cada página é buscada como uma requisição separada e cacheada independentemente. data é um array de resultados de página. Você precisa de .flat() ou .flatMap() para produzir uma única lista achatada para renderização.
getKey(pageIndex, previousPageData) é chamada para cada página começando no índice 0. Ela retorna a URL para essa página. Retorne null para sinalizar que não há mais páginas para buscar.
getKey computa a URL a partir de pageIndex (ex: ?offset=${pageIndex * 20}).getKey lê um cursor de previousPageData (ex: ?cursor=${previousPageData.nextCursor}).Use um IntersectionObserver no último elemento renderizado. Quando ele se torna visível, chame setSize(s => s + 1) para carregar a próxima página.
const { data } = useSWRInfinite(getKey, fetcher, {
parallel: true,
});Ela busca todas as páginas carregadas em paralelo durante a revalidação. Nota: isso só funciona para paginação baseada em offset, pois a baseada em cursor requer dados da página anterior.
Não. setSize(1) apenas controla quantas páginas são renderizadas, não o que está no cache. As páginas 2+ permanecem em cache. Isso pode levar a confusão se você espera que reduzir o tamanho limpe dados antigos.
Prefira revalidar a lista inteira após uma mutação em vez de atualizar manualmente páginas individuais. Atualizações manuais de cache em nível de página são complexas e propensas a erros com useSWRInfinite.
SWR pula o re-fetch da página 0 ao carregar páginas adicionais. Isso reduz as requisições de rede, mas significa que os dados da página 0 podem ficar desatualizados sem que o usuário perceba.
const { data } = useSWRInfinite<Product[]>(getKey, fetcher);
// data: Product[][] | undefined
const allProducts = data?.flat() ?? [];
// allProducts: Product[]O genérico tipa cada página no array de dados, então data se torna Data[].
Verifique os dados da última página. Se ela tiver menos itens que o limite da página ou estiver vazia, você atingiu o fim:
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE);Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥