import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback } from "react";
export function useTableUrlState() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const setParams = useCallback(
(updates: Record<string, string | number | null>) => {
const params = new URLSearchParams(searchParams.toString());
Object.entries(updates).forEach(([key, value]) => {
if (value === null || value === "") {
params.delete(key);
} else {
params.set(key, String(value));
}
});
router.push(`${pathname}?${params.toString()}`, { scroll: false });
},
[pathname, router, searchParams]
);
return { searchParams, setParams };
}Salutare! Am văzut mulți colegi la început de drum care copiază exemplul de DataTable direct din documentația shadcn/ui și se miră de ce li se blochează browserul când cresc datele. Exemplul lor oficial e scris pentru client-side, iar dacă ai nevoie de server-side pagination, sorting și filtering în Next.js App Router, trebuie să schimbi abordarea din temelii. Am avut exact cazul ăsta la un proiect recent cu peste 120k tranzacții lunare și vreau să vă arăt cum am rezolvat-o scurt și curat.
Problematica: Client-side vs URL-driven State
Când randezi un tabel server-side, sursa ta de adevăr nu mai trebuie să fie starea internă React din @tanstack/react-table (cum e un useState local), ci URL-ul. Fiecare pagină, sortare sau filtru trebuie să existe ca un searchParam (ex: ?page=2&sort=amount.desc&search=ion).
Dacă încerci să păstrezi starea în client și să faci fetch într-un useEffect, dai peste flash-uri urâte de UI, request-uri duplicate și o experiență de rahat dacă userul dă refresh sau vrea să trimită link-ul unui coleg. Totul trebuie să vină direct din page.tsx ca Server Component.
Flow-ul în Next.js App Router
Planul e simplu. Server Component-ul tău primește searchParams din URL, le validează (recomand zod), face query-ul în SQL sau ORM și trimite datele gata filtrate + pageCount către componenta de client <DataTable />.
Pe client, interceptăm evenimentele din TanStack Table (onPaginationChange, onSortingChange) și, în loc să actualizăm o stare locală, împingem noile valori direct în URL cu router.push. Experiența devine instantanee dacă folosești useTransition din React 18.
Trade-off sincer: Scrierea unui wrapper server-side adaugă cam 40-50 de linii de boilerplate pe fiecare tabel din aplicație dacă nu-ți faci un hook generoc. Totuși, câștigul de performanță e enorm. În cazul nostru, am redus First Input Delay de la 1.8 secunde la aproape zero și am economisit peste 40KB la JS bundle pe client, pentru că nu mai trimitem mii de obiecte JSON în browser doar ca să le filtreze clientul.
Debounce pe cautare si capcana cu paginarea
Cea mai frecventă greșeală pe care am văzut-o e lipsa debounce-ului la filtrarea text. Dacă userul tastează "Popescu" și tu trimiți 7 request-uri la server pentru fiecare literă, îți omori baza de date. Un useDebounce de 300ms la input-ul de căutare e obligatoriu.
O altă capcană e paginarea când aplici un filtru nou. Dacă userul se află pe pagina 15 și caută un termen care returnează doar 3 rezultate totale, tabelul va încerca să afișeze pagina 15 dintr-un total de 1 pagină. Rezultat: tabel gol. Regulă de aur: Ori de câte ori se schimbă căutarea sau un filtru, resetează întotdeauna parametrul page la 1.
Iată un hook simplu pe care îl folosesc pentru a converti evenimentele din TanStack Table în URL searchParams fără re-renderizări inutile:
Voi ce soluție folosiți pentru sincronizarea stării din tabel cu URL-ul în App Router? Margeți pe wrapper-ul nativ peste useRouter sau preferați librării gen nuqs?