import { useReactTable, getCoreRowModel, PaginationState, SortingState } from "@tanstack/react-table"
interface DataTableProps<TData> {
data: TData[]
pageCount: number
pagination: PaginationState
onPaginationChange: (pagination: PaginationState) => void
sorting: SortingState
onSortingChange: (sorting: SortingState) => void
}
export function useServerTable<TData>({ data, pageCount, pagination, onPaginationChange, sorting, onSortingChange }: DataTableProps<TData>) {
return useReactTable({
data,
columns,
pageCount,
state: { pagination, sorting },
manualPagination: true,
manualSorting: true,
manualFiltering: true,
onPaginationChange,
onSortingChange,
getCoreRowModel: getCoreRowModel(),
})
}Exemplul oficial de DataTable din shadcn/ui este excelent pentru mockup-uri sau liste de 30 de utilizatori, dar dacă îl arunci în producție pe un dataset serios, te lovești de perete. Am pățit asta recent la un modul de audit logs cu vreo 180.000 de înregistrări în PostgreSQL, unde clientul se plângea că pagina se blochează complet la filtrare.
Problema? Setup-ul implicit descarcă toate datele în browser și aplică getPaginationRowModel(), getSortedRowModel() și getFilteredRowModel() local pe client. Ca să trecem tabelul pe server-side, trebuie să oprim aceste plugin-uri interne și să delegăm logica către baza de date prin intermediul URL-ului.
Dezactivarea logicii client-side
TanStack Table știe nativ de operațiuni remote, însă trebuie să-i spunem explicit prin flag-urile manualPagination, manualSorting și manualFiltering. Fără ele, librăria va încerca să slice-uiască datele deja paginate primite de la server, afișând un tabel gol de la pagina 2 încolo.
Regula e simplă: când setezi manualPagination: true, trebuie să-i transmiți tabelei și pageCount-ul calculat pe server (Math.ceil(totalRows / pageSize)). Altfel, butoanele de "Next" și starea de pagină nu vor ști unde să se oprească.
Păstrarea stării în URL, nu în useState
Cea mai mare capcană pe care o văd în pull request-uri este ținerea paginării în useState local în interiorul componentei de tabel. Dacă userul sortează după data creării descrescător, filtrează pe status "FAILED" și dă refresh la pagină sau trimite link-ul unui coleg pe Slack, pierde tot contextul.
În Next.js App Router, abordarea sănătoasă este să legi starea direct la Search Params. Fie folosești un wrapper utilitar precum nuqs (recomand cu căldură dacă ai filtre complexe), fie împingi manual actualizările prin useRouter cu shallow / scroll: false:
- Server Component-ul citește
searchParamsși apelează baza de date (cuLIMIT,OFFSETși clauzeORDER BYpe indecși). - Datele vin direct pe props în Client Component (
<DataTable data={data} pageCount={totalPages} />). - Când apeși pe coloană să sortezi, declanșezi un
router.pushcare rescrie query string-ul.
Debounce la căutare și Trade-off-uri
Server-side filtering vine cu un cost de latență: fiecare tastă apăsată generează un network request. Nu lăsa input-ul de text legat direct la URL fără un debounce sănătos de 300-400ms, altfel îți bombardezi propriul API la fiecare literă tastată.
Trade-off-ul e destul de clar:
- Client-side câștigă dacă ai sub 500 de rânduri: navigarea între pagini e instantanee (0ms), sortarea nu atinge serverul, zero costuri de query.
- Server-side câștigă de la câteva mii de rânduri în sus: payload-ul inițial scade dramatic (la noi a scăzut de la un JSON de 14MB la doar 18KB per pagină), consumul de memorie în browser e minuscul, iar căutările se bazează pe indecșii full-text din SQL.
Voi cum gestionați sincronizarea filtrelor complexe? Mergeți pe nuqs sau v-ați scris propriile hook-uri de URL state?