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

Server-side pagination și filtrare cu shadcn/ui și TanStack Table în Next.js

De Delia Petre, 27 mai 2026 · 5 vizualizări · 2 like-uri

Postat 27 mai 2026
typescript
"use client";

import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useTransition } from "react";

export function useTableNavigation() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const [isPending, startTransition] = useTransition();

  const updateQuery = useCallback(
    (params: Record<string, string | null>) => {
      const newParams = new URLSearchParams(searchParams.toString());
      
      Object.entries(params).forEach(([key, value]) => {
        if (value === null || value === "") {
          newParams.delete(key);
        } else {
          newParams.set(key, value);
        }
      });

      startTransition(() => {
        router.push(`${pathname}?${newParams.toString()}`);
      });
    },
    [pathname, router, searchParams]
  );

  return { updateQuery, isPending };
}

Am văzut destui colegi care se blochează când trebuie să treacă de la exemplul de bază din shadcn/ui (care face totul pe client) la o implementare reală, server-side. Am pățit-o și eu la un proiect recent, un dashboard cu vreo 45.000 de înregistrări, unde varianta client-side pur și simplu îngenunchea browserul pe mobil. Soluția curată pentru Next.js App Router este să folosești URL-ul ca singură sursă de adevăr pentru starea tabelului.

De ce client-side e o capcană la volume mari

Când ai sub 100 de rânduri, e perfect să trântești tot JSON-ul în pagină și să lași TanStack Table să facă magia pe client. Dar când baza de date crește, loading-ul inițial explodează nepermis de mult. La proiectul de care ziceam, am redus timpul de încărcare inițial al paginii de la 4.2 secunde la doar 250ms mutând filtrarea și paginarea direct în query-ul de SQL.

În plus, dacă folosești URL-ul pentru stare, userul poate da copy-paste la link-ul paginii (de ex: /users?page=3&sort=email_desc&search=dan) și altcineva va vedea exact aceleași date filtrate. E un câștig uriaș pentru UX.

Cum legăm TanStack de URL Search Params

În loc să folosești useState local pentru pagination sau sorting din TanStack, vei citi aceste valori direct din Server Component-ul din Next.js (prin prop-ul searchParams) și le vei trimite ca prop-uri către Client Component-ul care randează efectiv tabelul.

Iată marele trade-off: scrii mult mai mult boilerplate. Trebuie să sincronizezi manual inputurile din UI cu URL-ul și să gestionezi stările de tranzit. Nu mai e o soluție rapidă de tipul "plug and play".

Un helper simplu pentru update-ul URL-ului

Cea mai mare problemă pe client este să modifici un parametru (de exemplu, pagina curentă) fără să le pierzi pe celelalte deja existente (cum ar fi filtrul de căutare sau sortarea). Pentru asta, ne putem scrie un custom hook simplu care folosește useTransition din React pentru a nu bloca UI-ul în timpul navigării.

Ce am învățat pe pielea mea (Gotchas)

  1. Debounce obligatoriu la search: Dacă ai un input de căutare în tabel și faci push în router la fiecare tastă apăsată, vei bombarda serverul cu request-uri API inutile. Pune un debounce de cel puțin 300ms înainte de a actualiza URL-ul.
  2. Validarea parametrilor pe backend: Nu avea încredere oarbă în ce vine din searchParams. Cineva poate scrie manual ?sort=drop_tables. Eu folosesc Zod pe server pentru a parsa și valida parametrii înainte de a-i trimite în query-ul de Prisma sau Drizzle.
  3. Loading states vizibile: Când schimbi pagina, Next.js va face un nou request pe server. Folosește starea isPending returnată de useTransition ca să aplici o opacitate pe tabel, altfel userul va crede că aplicația a înghețat până se încarcă noile date.

Cum gestionați voi tabelele complexe în Next.js? Scrieți manual push-urile în router sau folosiți librării extra gen nuqs ca să scăpați de boilerplate?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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