import { useReactTable, getCoreRowModel, PaginationState, SortingState } from '@tanstack/react-table'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { useCallback } from 'react'
interface DataTableProps<TData> {
data: TData[]
pageCount: number
columns: any[]
}
export function ServerDataTable<TData>({ data, pageCount, columns }: DataTableProps<TData>) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const page = Number(searchParams.get('page')) || 1
const perPage = Number(searchParams.get('per_page')) || 10
const createQueryString = useCallback((params: Record<string, string | number | null>) => {
const newParams = new URLSearchParams(searchParams.toString())
Object.entries(params).forEach(([key, value]) => {
if (value === null) newParams.delete(key)
else newParams.set(key, String(value))
})
return newParams.toString()
}, [searchParams])
const table = useReactTable({
data,
columns,
pageCount,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
state: {
pagination: { pageIndex: page - 1, pageSize: perPage },
},
onPaginationChange: (updater) => {
const next = typeof updater === 'function' ? updater({ pageIndex: page - 1, pageSize: perPage }) : updater
router.push(`${pathname}?${createQueryString({ page: next.pageIndex + 1, per_page: next.pageSize })}`)
},
getCoreRowModel: getCoreRowModel(),
})
// Return tabelul shadcn standard folosind instanța 'table'...
}Toată lumea iubește shadcn/ui când dă copy-paste la componenta de DataTable. Pui un array de 50 de obiecte în mock data, bifezi sortarea pe coloane și zici că ai terminat treaba pe ziua respectivă.
Am pățit-o pe un proiect intern tip ERP unde aveam vreo 140k tranzacții. În primele două săptămâni totul zbura pentru că testam pe 20 de rânduri. Când am tras prima bază de date de staging pe client, browserul a înghețat instant: descărcam un payload JSON de 6.8MB și procesam sortările pe thread-ul principal din browser.
Soluția e evident server-side pagination, dar TanStack Table v8 ascunde câteva capcane dacă încerci să-l legi direct de Next.js App Router.
De ce URL state bate useState local
Prima greșeală pe care o văd frecvent este ținerea stării de paginare (pageIndex, pageSize, sorting, filters) în useState local în interiorul componentei de tabel. Pare curat, dar pierzi trei chestii critice: deep-linking (nu poți trimite linkul către pagina 4 cu filtrul 'status=pending'), istoricul din browser (butonul de Back nu face nimic) și Server Components.
Cea mai curată variantă este să tratezi searchParams ca singura sursă de adevăr (Single Source of Truth). Fie folosești direct useSearchParams și useRouter din next/navigation, fie tragi librăria nuqs (fostul next-usequerystate) dacă vrei type-safety automat.
Configurația manuală din TanStack Table
Ca să-i spui lui TanStack că nu mai vrea să filtreze sau să pagineze el în memorie, trebuie să activezi flag-urile manuale. Dacă uiți de manualPagination: true sau nu pasezi pageCount, tabelul își recalculează paginile crezând că array-ul tău de 10 elemente primit de pe server reprezintă tot setul de date.
Când userul dă click pe paginare sau schimbă un filtru, nu updatezi direct starea tabelului, ci împingi noii parametri în URL cu shallow: false (sau router.push('?page=2')). Next.js re-randează Server Component-ul părinte, face query-ul în SQL cu skip/take și trimite noul subset de date înapoi.
Trade-off-uri din producție
Abordarea asta vine cu câteva compromisuri pe care trebuie să le accepți:
- Debouncing obligatoriu pe filtre text: Dacă tragi un input de căutare pe coloana de 'Nume client', trebuie să pui un debounce de minim 300ms înainte să împingi în URL. Altfel omori baza de date cu 10 query-uri consecutive la fiecare tastă apăsată.
- Loading states / Suspense: Când paginarea e pe server, ai o latență de rețea între click și update. Trebuie neapărat să împachetezi tabelul într-un
useTransitionca să ții UI-ul responsiv și să afișezi un opacity scăzut sau un skeleton pe rânduri în timp ceisPendinge true. - Boilerplate mai mare: Trecerea de la client-side la server-side triplează cantitatea de cod din jurul tabelei. Dacă ai tabele mici (sub 500 de înregistrări, cum ar fi o listă de categorii sau tag-uri), nu merită efortul — lasă totul pe client cu
getFilteredRowModel().
Voi cum gestionați tabelele complexe în App Router? Mergeți pe nuqs sau încă preferați fetch client-side cu TanStack Query (React Query) ca să nu atingeți URL-ul?