React
Paginazione con searchParams nell’App Router
La paginazione legge page da searchParams e resta nell’URL: codice, differenza tra offset e keyset, e cosa mettere in cache.
In sintesi
- `page` arriva da `searchParams`, che in Next 15 è una Promise e va atteso.
- La paginazione con `` tiene l’URL condivisibile e indicizzabile.
- Offset scarta righe, keyset no: la scelta dipende dal volume, non dal gusto.
La paginazione nell’App Router legge page da searchParams, non dallo stato del client: così l’URL è condivisibile e la pagina resta indicizzabile. In Next 15 searchParams è una Promise e va atteso.
La firma della pagina
// app/blog/page.tsx
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise;
}) {
const { page } = await searchParams;
const parsed = Number(page ?? 1);
const current = Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : 1;
const perPage = 12;
const posts = await getPosts({ skip: (current - 1) * perPage, take: perPage });
return ;
}
Il cast va protetto. Un valore non numerico produce un offset invalido, e il database risponde con un errore invece che con la prima pagina. Number.isFinite e un clamp a 1 chiudono il caso senza eccezioni.
Link, non stato
import Link from "next/link";
Pagina successiva
La paginazione si fa con un link, non con un bottone che cambia stato. Il prefetch di dieci link di paginazione è traffico sprecato: prefetch={false} sui numeri tiene la navigazione veloce senza scaricare pagine che nessuno aprirà.
| Approccio | URL condivisibile | Indicizzabile | Costo |
|---|---|---|---|
| searchParams più Link | Sì | Sì | Una richiesta per pagina |
| Stato del client | No | No | Nessuna |
| Scroll infinito | Solo la prima pagina | Parziale | Molte richieste |
Offset o keyset
OFFSET 120 LIMIT 12 rallenta quando la tabella cresce, perché il database legge e scarta le righe precedenti. Con un ordinamento stabile su createdAt, id la keyset pagination chiede WHERE (createdAt, id) < (?, ?) e il costo resta costante.
- Offset: semplice, numeri di pagina visibili, adatto fino a qualche migliaio di righe.
- Keyset: veloce a ogni profondità, ma niente salto diretto a pagina 7.
- Conteggio totale: una query in più, da mettere in cache con una scadenza.
Sul catalogo EneUI, con 111.985 componenti e 1.556 blocchi misurati il 1 novembre 2026 su ui.eneawork.it, una griglia da 24 elementi produce oltre 4.600 pagine: il conteggio totale va in cache, non ricalcolato a ogni visita.
Componenti per la griglia e i controlli
Tre record: i componenti data table per la variante con colonne, il blocco data-table già composto, e i blocchi tabella per le liste dense.
Il comportamento della cache attorno a una lista paginata è in i quattro livelli di cache; il rendering progressivo della prima pagina in streaming e Suspense. Il confronto con un altro framework è in Remix vs Next.js.
Il limite da mettere in conto
La paginazione con searchParams rende ogni pagina una URL distinta: utile per la SEO, costoso per la cache se generi 4.600 pagine. Indicizza le prime e metti noindex oltre una soglia che decidi tu.
Domande frequenti
searchParams è asincrono?
Sì, da Next 15 è una Promise. Va atteso con <code>await</code> sia nella pagina sia in <code>generateMetadata</code>. Leggerlo come oggetto sincrono produce un errore di tipo.
Perché non uso lo stato del client?
Perché perdi l’URL condivisibile e la pagina 2 non esiste per i crawler. Lo stato del client resta utile per filtri temporanei che non vuoi nell’indice.
Meglio offset o keyset?
Offset fino a qualche migliaio di righe, keyset oltre. La keyset richiede un ordinamento stabile e un cursore nell’URL, quindi niente salto a una pagina arbitraria.
Come cacheo il conteggio totale?
Con <code>unstable_cache</code> o una cache HTTP con scadenza, non con una query a ogni richiesta. Un conteggio che cambia di poche unità non vale il costo.
Devo indicizzare tutte le pagine?
No. Le prime tre o cinque bastano ai crawler e agli utenti. Oltre quella soglia <code>noindex, follow</code> evita migliaia di pagine quasi identiche nell’indice.