import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useCallback } from "react";
export function useTableNavigation() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
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 goToPage = (pageIndex: number) => {
const query = createQueryString({ page: pageIndex + 1 });
router.push(`${pathname}?${query}`, { scroll: false });
};
return { goToPage, searchParams };
}Am implementat recent o tabelă complexă cu shadcn/ui și TanStack Table pe un proiect cu peste 150.000 de înregistrări în baza de date. Dacă lași totul pe client-side, așa cum îți arată exemplele de bază din documentația shadcn, la peste 1000 de rânduri deja începe să agațe serios browserul. Iar la 150k, pur și simplu îți moare tab-ul de Chrome.
Soluția evidentă este să muți sortarea, paginarea și filtrarea direct pe server. Dar implementarea nu e chiar atât de simplă pe cât pare în teorie, mai ales dacă vrei un UX impecabil și un cod curat în Next.js App Router (14/15).
De ce starea tabelului trebuie să trăiască în URL
Am văzut mulți devi care țin starea paginării (pageIndex, pageSize) în useState la nivel de client component. Am făcut și eu greșeala asta la început. Problema? Când userul trimite un link unui coleg sau dă refresh la pagină, pierde complet contextul filtrării și se trezește înapoi pe pagina 1, cu toate filtrele resetate.
Soluția elegantă este să folosim URL-ul ca "single source of truth". Parametrii precum ?page=3&sort=email_desc&search=popescu sunt ideali. În Next.js, Server Component-ul citește searchParams, face query-ul direct în baza de date (cu Prisma, Drizzle sau ce folosești) și trimite doar rândurile necesare către client component.
Cel mai mare trade-off: Latența și atacarea bazei de date
Deși abordarea server-side salvează memoria browserului, introduci un nou inamic: latența rețelei.
La fiecare apăsare de tastă în inputul de căutare, se declanșează un request către server. Pe un server local de dezvoltare totul pare instant. Însă, când am urcat proiectul pe un VPS ieftin de 5$, baza de date a intrat în kernel panic după ce trei useri au început să tasteze rapid în același timp. Fiecare literă trimitea un query LIKE %text% extrem de costisitor.
Pentru a evita asta, trebuie neapărat să folosești un debounce de 300-400ms pe input-ul de filtrare înainte de a actualiza URL-ul. De asemenea, folosește useTransition din React pentru a păstra UI-ul interactiv în timp ce Next.js face fetch la noile date în fundal.
Cum sincronizezi TanStack Table cu URL-ul din Next.js
Secretul este să nu folosești starea internă a TanStack Table pentru controlul paginării, ci să o controlezi manual prin proprietățile tabelului, legându-le direct de routerul din Next.js.
Iată mai jos un helper simplu pe care îl folosesc pentru a genera URL-ul corect fără a pierde celelalte filtre deja existente în query string.
Cum gestionăm sortarea
La sortare, lucrurile devin și mai interesante. TanStack Table suportă sortare multi-coloană nativ, dar pe server-side devine un coșmar de tradus în SQL. Sfatul meu sincer: limitează-te la sortare pe o singură coloană dacă nu ai un caz de utilizare critic în care chiar se cere multi-sort. E mult mai simplu să parsezi ?sort=createdAt.desc în backend decât o matrice întreagă de sortări.
În plus, asigură-te că ai indecși pe coloanele pe care permiți sortarea și filtrarea. Altfel, când tabela crește, paginarea la finalul listei (ex. OFFSET 140000) va deveni extrem de înceată, chiar dacă folosești server-side pagination.
Concluzie
Server-side pagination cu shadcn/ui și TanStack Table este singura cale viabilă pentru date reale de producție. Mutarea stării în URL rezolvă UX-ul (shareable links), dar te obligă să fii foarte atent la debounce și la performanța bazei de date.
Voi cum gestionați debounce-ul pe filtrele din URL în Next.js? Folosiți o librărie dedicată sau ați scris un hook custom cu useSearchParams și useRouter?