import { useCallback, useTransition } from 'react'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
export function useTableNavigation() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [, startTransition] = useTransition()
const updateParams = useCallback((newParams: Record<string, string | null>) => {
const params = new URLSearchParams(searchParams.toString())
Object.entries(newParams).forEach(([key, value]) => {
if (value === null || value === '') {
params.delete(key)
} else {
params.set(key, value)
}
})
startTransition(() => {
router.push(`${pathname}?${params.toString()}`, { scroll: false })
})
}, [router, pathname, searchParams])
return { updateParams }
}S-ar putea să crezi că shadcn/ui îți dă un tabel gata de producție direct din cutie, dar când treci de la client-side la server-side pe un set mare de date, lucrurile devin rapid complicate. Am avut recent un proiect cu peste 15.000 de utilizatori unde trebuia să facem sortare, filtrare și paginare direct pe server ca să nu omorâm browserul. Soluția curată în Next.js App Router este să ții toată starea tabelului direct în URL (query params), nu în useState.
De ce în URL? Simplu. Dacă trimiți un link unui coleg, vrei ca el să vadă exact aceeași pagină, cu aceleași filtre aplicate și pe aceeași pagină 3. Dacă folosești useState clasic în interiorul tabelului, la un simplu refresh pierzi tot progresul. În plus, butonul de "Back" din browser funcționează natural.
Sincronizarea stării: URL-ul este sursa unică de adevăr
Pentru a face asta, citim parametrii din searchParams direct în pagina de server (page.tsx), îi trimitem către baza de date ca să luăm datele paginate, și apoi îi pasăm către componenta client-side a tabelului ca starea sa inițială.
Când utilizatorul interacționează cu tabelul (schimbă pagina, sortează, caută), nu modificăm starea React internă a tabelului, ci modificăm URL-ul folosind router.push. TanStack Table are nevoie de mici adaptări pentru asta. În loc să lași tabelul să își gestioneze singur starea de paginare, trebuie să controlezi proprietățile pageIndex și pageSize manual, legându-le la valorile extrase din URL.
Trade-off-ul de performanță pe care trebuie să-l accepți
Nimic nu e gratis în meseria asta, iar abordarea asta vine cu un preț. Dacă filtrezi la fiecare tastă apăsată (la un input de search clasic), vei trimite zec zi de request-uri către server în timp ce utilizatorul tastează. Asta înseamnă că baza ta de date va fi bombardată inutil.
Ca să evităm asta, am aplicat două tactici esențiale pe care le recomand oricui:
- Am pus un debounce de minimum 300ms pe input-ul de filtrare. Astfel, facem update la URL doar când userul s-a oprit din scris.
- Am folosit
useTransitionde la React. Împachetarea navigării înstartTransitionîi spune React-ului că update-ul de URL este o acțiune necritică. UI-ul rămâne interactiv și nu îngheață în timp ce Next.js face fetch la datele noi în fundal.
La proiectul menționat, unde aveam query-uri SQL destul de complexe cu join-uri, optimizarea asta cu debounce și useTransition ne-a scăzut încărcarea pe baza de date cu aproape 40% în timpul orelor de vârf.
La sortare, TanStack îți dă un array de tipul [{ id: 'email', desc: true }]. În handler-ul de onSortingChange, trebuie să traduci acest obiect într-un string simplu pe care URL-ul îl poate digera ușor (de exemplu, ?sort=email.desc), pe care apoi îl trimiți către API-ul tău sau direct în query-ul de Prisma ori Drizzle.
Voi cum gestionați tabelele mari în Next.js? Mergeți pe varianta asta cu starea în URL sau preferați să faceți fetch-uri clasice prin API routes și să țineți totul în state local?