eduardweb.
shadcn/uiAvansat#performance#nextjs#react#shadcn-ui#tanstack-table

Cum legi shadcn DataTable de server-side pagination și filtrare în Next.js

De Andreea Crăciun, 7 sept. 2026 · 20 vizualizări · 2 like-uri

Postat 7 sept. 2026
typescript
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
  pageIndex: Number(searchParams.get('page') ?? 1) - 1,
  pageSize: Number(searchParams.get('per_page') ?? 10),
});

const [isPending, startTransition] = useTransition();

const table = useReactTable({
  data,
  columns,
  pageCount: Math.ceil(totalCount / pageSize),
  state: { pagination: { pageIndex, pageSize } },
  manualPagination: true,
  manualSorting: true,
  manualFiltering: true,
  onPaginationChange: (updater) => {
    const next = typeof updater === 'function' ? updater({ pageIndex, pageSize }) : updater;
    setPagination(next);
    startTransition(() => {
      const params = new URLSearchParams(searchParams.toString());
      params.set('page', String(next.pageIndex + 1));
      params.set('per_page', String(next.pageSize));
      router.push(`?${params.toString()}`, { scroll: false });
    });
  },
  getCoreRowModel: getCoreRowModel(),
});

Când ai 50 de rânduri într-un mock, implementarea default din shadcn/ui merge impecabil. Problema începe când ai 45.000 de comenzi într-un magazin online și observi că browserul descarcă un payload de 18MB doar ca să le filtreze pe client.

Am pățit fix asta acum vreo 6 luni pe un panou de admin. Soluția sănătoasă nu e să renunți la TanStack Table, ci să-l forțezi să lucreze server-side, legând starea tabelei direct la URL.

De ce URL searchParams și nu useState local?

Cea mai mare greșeală pe care o văd în pull request-uri este ținerea paginării în useState. Dacă agentul de la suport filtrează după utilizatori suspendați, ajunge la pagina 4, deschide un tichet și apoi dă refresh, pierde tot contextul. Mai rău: dacă vrea să trimită link-ul unui coleg pe Slack, URL-ul e doar /admin/users.

Punând page, per_page, sort și filter direct în searchParams, bifezi două lucruri dintr-o lovitură: URL-ul devine sursa unică de adevăr (bookmarkable, share-able), iar Next.js Server Components pot citi direct acești parametri pentru a face query-ul SQL cu LIMIT și OFFSET înainte de randare.

Triada manuală pe care uită toată lumea s-o seteze

TanStack Table e proiectat implicit să facă totul în memorie. Dacă îi trimiți doar 10 rânduri de la server, dar nu-i spui explicit că lucrează manual, componenta va presupune că acele 10 rânduri reprezintă tot setul de date. Paginarea va arăta o singură pagină, iar sortarea va reordona doar acele 10 elemente.

Trebuie neapărat să pasezi manualPagination: true, manualSorting: true și manualFiltering: true, plus pageCount-ul calculat pe backend (ex: Math.ceil(totalRows / pageSize)). Am pierdut două ore bune prima oară când m-am lovit de asta, întrebându-mă de ce nu se schimbă pagina deși serverul returna datele corecte.

Trade-off-uri și capcana debounce-ului

Să muți totul pe server nu e gratis. Principalul compromis este latența: fiecare click pe un header de coloană pentru sortare declanșează un round-trip către server și un query în Postgres.

Pentru sortare și paginare, împachetează navigarea într-un useTransition. În felul ăsta, tabela nu îngheață, iar Next.js îți expune un flag de isPending pe care îl poți folosi ca să reduci opacitatea rândurilor existente cât timp se încarcă noul set.

A doua capcană: filtrarea după text. Dacă trimiți un request la server la fiecare tastă apăsată în inputul de căutare, îți îngenunchezi baza de date. Un debounce de 300-400ms nu e opțional aici, e obligatoriu.

Voi cum abordați tabelele astea mari în App Router: lăsați Server Components să facă fetching pe baza searchParams-ului din page.tsx sau preferați un Client Component cu SWR/React Query legat la un route handler dedicat?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.