# 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.

> Pagina: https://ui.eneawork.it/blog/nextjs-paginazione-search-params
> Markdown: https://ui.eneawork.it/blog/nextjs-paginazione-search-params.md

*2026-11-01*

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à.

## 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.

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.

URL per lo stato che vuoi condividere, stato locale per quello che dura un clic. La pagina è un dato, non un effetto.

## FAQ

### searchParams è asincrono?

Sì, da Next 15 è una Promise. Va atteso con await sia nella pagina sia in generateMetadata. 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 unstable_cache 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 noindex, follow evita migliaia di pagine quasi identiche nell’indice.
