import { useReactTable, getCoreRowModel, type PaginationState, type SortingState } from "@tanstack/react-table"
import { useRouter, usePathname, useSearchParams } from "next/navigation"
export function useDataTableServer({ pageCount }: { pageCount: number }) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const page = Number(searchParams.get("page")) || 1
const pageSize = Number(searchParams.get("per_page")) || 10
const pagination: PaginationState = { pageIndex: page - 1, pageSize }
const setPagination = (updater: any) => {
const next = typeof updater === "function" ? updater(pagination) : updater
const params = new URLSearchParams(searchParams.toString())
params.set("page", String(next.pageIndex + 1))
params.set("per_page", String(next.pageSize))
router.push(`${pathname}?${params.toString()}`)
}
return { pagination, setPagination, pageCount }
}Am refăcut recent un dashboard intern pentru un client din logistică unde aveam vreo 140k comenzi în Postgres. Toată lumea copiază exemplul de DataTable direct din documentația shadcn/ui, dar acel cod e gândit exclusiv pentru filtrare și paginare pe client. La 500 de rânduri merge brici, însă la 10k+ rânduri browserul îngheață și descarci zeci de megabytes de date complet inutil.
De ce abordarea implicită te lasă la greu
Componenta din shadcn/ui e practic un wrapper curat peste TanStack Table v8. În modul client-side, TanStack ține tot dataset-ul în memorie și se ocupă el de slice, filter și sort. Când treci în producție cu volume reale, abordarea asta moare rapid.
Regula mea e simplă: URL-ul trebuie să fie mereu single source of truth. Dacă utilizatorul trimite un link pe Slack către un coleg, colegul trebuie să vadă exact aceeași pagină, cu aceleași filtre și aceeași sortare activă (?page=2&sort=createdAt.desc&status=pending).
Pentru asta, trebuie să-i spui explicit tabelei că datele vin deja procesate de server:
manualPagination: truemanualSorting: truemanualFiltering: true
Legătura cu Server Components și URL State
În Next.js App Router, cea mai elegantă variantă este să citești searchParams direct în Server Component (pagina principală), să rulezi query-ul SQL cu LIMIT și OFFSET (sau keyset pagination dacă ai volume masive), și să pasezi datele gata feliate către componenta client a tabelei.
Pentru manipularea URL-ului din client, am renunțat demult la a scrie manual logica cu useRouter și URLSearchParams. Folosesc nuqs (fostul next-usequerystate) sau un custom hook mic cu useTransition. useTransition e esențial aici: ține interfața responsivă în timp ce Server Component-ul refetch-uiește datele pe fundal, fără să-ți blocheze inputurile.
Capcana clasică: debounce-ul și resetarea paginii
Am văzut greșeala asta inclusiv la seniori: legi un input de căutare direct la router fără debounce. La fiecare literă tastată, trimiți un request la server. Dacă ai 8k useri activi concomitent, ai pus baza de date în genunchi în cinci minute.
Al doilea bug frecvent: cauți ceva când ești pe pagina 14. Dacă rezultatul căutării are doar 2 pagini, utilizatorul vede o tabelă goală și crede că datele lipsesc. Când schimbi un filtru sau inputul de căutare, parametrul page trebuie resetat obligatoriu la 1.
Trade-off-ul e destul de clar: pierzi viteza aceea instantanee de pe client unde filtrarea se întâmplă în 2 milisecunde fără network roundtrip, și trebuie să construiești skeleton loaders pentru starea de loading. În schimb, tabela consumă câțiva kiloocteți de memorie în browser, iar consumul rămâne constant indiferent dacă în baza de date ai o mie sau un milion de rânduri.
Voi cum gestionați search params la tabele complexe: mergeți pe biblioteci gen nuqs sau preferați să țineți starea în URL doar pe event-uri explicite?