Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Usa useSWRInfinite para gestionar datos paginados y de scroll infinito. Define una función getKey que devuelva la URL correcta para cada índice de página, y llama a setSize para cargar más 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; // se alcanzó el final
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 ? "Cargando..." : isReachingEnd ? "No hay más publicaciones" : "Cargar más"}
</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) => {
// Primera página - no se necesita cursor
if (pageIndex === 0) return "/api/products?limit=20";
// No hay más páginas
if (!previousPageData?.nextCursor) return null;
// Paginación basada en 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>Cargando productos...</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)}>Cargar más</button>
)}
</div>
);
}useSWRInfinite gestiona un array de páginas, donde cada página es el resultado de una obtención separada.getKey(pageIndex, previousPageData) se invoca para cada índice de página empezando desde 0.null desde getKey indica a SWR que deje de obtener más páginas.data es un array de arrays (o un array de objetos de página), una entrada por cada página cargada.setSize(n) establece cuántas páginas deben cargarse. SWR obtiene cualquier página que aún no esté en caché.Paginación basada en offset:
const getKey = (pageIndex: number) => `/api/items?offset=${pageIndex * 20}&limit=20`;Con 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>
);
}Obtención paralela de páginas:
const { data } = useSWRInfinite(getKey, fetcher, {
parallel: true, // Obtener todas las páginas en paralelo durante la revalidación
});useSWRInfinite<Data, Error> tipan cada página en el array de datos.data es Data[] | undefined - un array de páginas.getKey recibe (pageIndex: number, previousPageData: Data | null).const { data } = useSWRInfinite<Product[]>(getKey, fetcher);
// data: Product[][] | undefined
const allProducts = data?.flat() ?? [];
// allProducts: Product[]data es un array de páginas, no una lista plana. Casi siempre necesitas .flat() o .flatMap() para obtener un único array.revalidateFirstPage: false puede evitar reobtenciones innecesarias de la página 0 al cargar páginas adicionales, pero significa que los datos de la página 0 podrían quedar obsoletos.setSize dispara obtenciones para cualquier página que aún no esté en caché. Establecer setSize(1) no descarta las páginas 2+ de la caché; solo controla lo que se renderiza.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| useSWRInfinite | Integrado en SWR, caché a nivel de página | Lógica de getKey compleja, forma de datos array-de-arrays |
| useSWR manual por página | Modelo mental más simple | Sin "cargar más" ni seguimiento de páginas integrados |
| React Query useInfiniteQuery | API similar, getNextPageParam integrado | Ecosistema de biblioteca diferente |
| Server Components con searchParams | Sin JS en el cliente, apto para SEO | Recarga completa de página en cada cambio de página |
Cada página se obtiene como una solicitud separada y se cachea de forma independiente. data es un array de resultados de página. Necesitas .flat() o .flatMap() para producir una única lista plana para renderizar.
getKey(pageIndex, previousPageData) se invoca para cada página empezando en el índice 0. Devuelve la URL de esa página. Devuelve null para indicar que no hay más páginas que obtener.
getKey calcula la URL a partir de pageIndex (p. ej., ?offset=${pageIndex * 20}).getKey lee un cursor de previousPageData (p. ej., ?cursor=${previousPageData.nextCursor}).Usa un IntersectionObserver en el último elemento renderizado. Cuando se vuelve visible, llama a setSize(s => s + 1) para cargar la página siguiente.
const { data } = useSWRInfinite(getKey, fetcher, {
parallel: true,
});Obtiene todas las páginas cargadas en paralelo durante la revalidación. Nota: esto solo funciona para paginación basada en offset, ya que la basada en cursor requiere los datos de la página anterior.
No. setSize(1) solo controla cuántas páginas se renderizan, no lo que hay en la caché. Las páginas 2+ permanecen en caché. Esto puede generar confusión si esperas que reducir el tamaño borre los datos antiguos.
Prefiere revalidar toda la lista después de una mutación en lugar de actualizar manualmente páginas individuales. Las actualizaciones manuales de caché a nivel de página son complejas y propensas a errores con useSWRInfinite.
SWR omite reobtener la página 0 cuando se cargan páginas adicionales. Esto reduce las solicitudes de red, pero significa que los datos de la página 0 pueden quedar obsoletos sin que el usuario se dé cuenta.
const { data } = useSWRInfinite<Product[]>(getKey, fetcher);
// data: Product[][] | undefined
const allProducts = data?.flat() ?? [];
// allProducts: Product[]El genérico tipa cada página en el array de datos, por lo que data se convierte en Data[].
Comprueba los datos de la última página. Si tiene menos elementos que el límite de la página o está vacía, has llegado al final:
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE);Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥