"use client"
import { useCallback, useTransition } from "react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { PaginationState } from "@tanstack/react-table"
export function useDataTableServerParams() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [isPending, startTransition] = useTransition()
const createQueryString = useCallback(
(params: Record<string, string | number | null>) => {
const newParams = new URLSearchParams(searchParams.toString())
for (const [key, value] of Object.entries(params)) {
if (value === null || value === "") {
newParams.delete(key)
} else {
newParams.set(key, String(value))
}
}
return newParams.toString()
},
[searchParams]
)
const onPaginationChange = (updater: PaginationState | ((old: PaginationState) => PaginationState)) => {
const currentPagination: PaginationState = {
pageIndex: Number(searchParams.get("page") ?? "1") - 1,
pageSize: Number(searchParams.get("per_page") ?? "10"),
}
const next = typeof updater === "function" ? updater(currentPagination) : updater
startTransition(() => {
router.push(
`${pathname}?${createQueryString({
page: next.pageIndex + 1,
per_page: next.pageSize,
})}`,
{ scroll: false }
)
})
}
return { onPaginationChange, isPending }
}Toată lumea sare pe shadcn/ui DataTable pentru că arată bine din prima și exemplul lor din documentație cu TanStack Table funcționează brici... pe date mock de 20 de rânduri. Problema e când treci în producție și ai de randat 15.000 de comenzi în dashboard, iar client-side pagination îți omoară browserul și consumă 200MB de RAM.
Anul trecut am refăcut un ERP intern unde aveam un tabel cu peste 80k loguri de audit. Am încercat prima dată varianta clasică client-side: încărcam totul dintr-un API route și lăsam React Table să facă slice, filter și sort pe client. Rezultatul? Initial page load de 4 secunde și un main thread înghețat la fiecare tastă apăsată în search bar.
Am mutat totul server-side folosind searchParams din Next.js App Router. Iată ce am învățat din asta și unde sunt capcanele cele mai mari.
De ce URL-ul este Single Source of Truth
Cea mai mare greșeală pe care o vezi în tutoriale e păstrarea stării de filtrare și paginare în useState local pe client. Când userul dă refresh, pierde tot. Când trimite un link colegului din support, colegul vede prima pagină nefiltrată, nu pagina 4 cu filtrul status=pending pe care o urmărea el.
În App Router, tabelul tău trebuie să citească starea direct din URL pe server (RSC) și să execute query-ul direct în baza de date (Prisma, Drizzle, ce folosești tu).
Mecanismul curat funcționează așa:
- Userul schimbă pagina sau aplică un filtru.
- Modifici URL-ul cu
router.push('?page=2&sort=desc')la nivel de client. - Server Component-ul re-randază, face query SQL optimizat cu
LIMITșiOFFSET(sau keyset pagination). - Trimiți doar cele 10-20 de rânduri necesare peste rețea către client.
Capcana re-render-urilor și Debounce pe Search
Aici se rupe filmul la 90% din implementări: dacă legi un <Input /> de search direct la router.push, la fiecare literă tastată declanșezi un server request. Dacă cineva caută "popescu", trimiți 7 request-uri la server în 300 de milisecunde.
Trebuie să folosești un debounce pe inputul de căutare și să împachetezi update-ul de router într-un useTransition. La proiectul meu cu 12k utilizatori activi, trecerea la un debounce de 300ms plus router transitions ne-a redus încărcarea pe baza de date cu aproape 40% în orele de vârf.
Trade-off-uri: Server-Side vs. Client-Side
Nimic nu e gratis în software architecture. Când treci la server-side DataTables:
- Câștigi: Bundle size mic pe client, memorie RAM consumată minimă, link-uri share-uibile și scalabilitate până la milioane de rânduri.
- Pierzi: Latență la fiecare acțiune (ai un roundtrip de rețea, chiar dacă e minim), iar animațiile fluide de layout devin mai greu de sincronizat.
Dacă ai sub 500 de înregistrări în tabel, sincer, rămâi pe client-side. Nu-ți complica viața degeaba. Dar când treci de câteva mii de rânduri, arhitectura bazată pe URL devine obligatorie.
Sincronizarea TanStack Table cu Next.js Router
Codul de mai jos arată un helper hook custom pe care îl folosesc pentru a sincroniza schimbările de paginare din TanStack Table direct în URL-ul din Next.js, păstrând în același timp indicatorul de încărcare isPending intact.
Voi cum gestionați paginarea la volume mari în App Router? Mergeți pe searchParams standard, folosiți librării gen nuqs, sau preferați API routes apelate din TanStack Query pe client?