Zaawansowane tabele danych z TanStack Table w Next.js
TanStack Table to potężna biblioteka bez opinii na temat renderowania. Jak zbudować z nią tabelę z sortowaniem, filtrowaniem i paginacją w Next.js?
Maciej Sala
Founder StriveLab
5 min czytaniaOpublikowano 11 kwietnia 2026 (Aktualizacja 17 lipca 2026)
Dlaczego TanStack Table w Next.js?
Słowo oznacza tu jedno: biblioteka nie narzuca Ci ani jednego piksela wyglądu. Całą warstwę wizualną budujesz sam, Tailwindem, dokładnie tak, jak chcesz.
Definicje cell i header są funkcjami, więc nie są serializowalnymi propsami Reacta. Zaimportuj je po stronie klienta i przekaż z Server Component tylko dane:
Code
// app/dashboard/orders/orders-table.tsx'use client'import { DataTable } from '@/components/data-table'import { orderColumns, type Order } from './columns'export function OrdersTable({ data }: { data: Order[] }) { return ( <DataTable data={data} columns={orderColumns} getRowId={(order) => order.id} caption="Lista zamówień" /> )}
TanStack Table w Next.js: Server Component i Client Table
Code
// app/dashboard/orders/page.tsximport { db } from '@/lib/db'import { OrdersTable } from './orders-table'import type { Order } from './columns'export default async function OrdersPage() { const records = await db.order.findMany({ select: { id: true, customerName: true, email: true, total: true, status: true, createdAt: true, }, orderBy: { createdAt: 'desc' }, take: 500, // Limit biznesowy, nie uniwersalny próg wydajności. }) const orders: Order[] = records.map((order) => ({ ...order, total: Number(order.total), createdAt: order.createdAt.toISOString(), })) return ( <div> <h1 className="mb-6 text-2xl font-bold">Zamówienia</h1> <OrdersTable data={orders} /> </div> )}
Dane są pobierane w Server Component, normalizowane do serializowalnego DTO i renderowane w Client Component. Nie traktuj ukrycia kolumny jako zabezpieczenia: wszystko, co przekażesz w data, użytkownik może odczytać w payloadzie RSC. Autoryzację i wybór pól wykonuj na serwerze.
Selekcja wierszy w tabeli danych
Code
// Dodaj do columns na początku:{ id: 'select', header: ({ table }) => ( <input type="checkbox" aria-label="Zaznacz wszystkie wiersze na tej stronie" aria-checked={ table.getIsSomePageRowsSelected() ? 'mixed' : table.getIsAllPageRowsSelected() } checked={table.getIsAllPageRowsSelected()} onChange={table.getToggleAllPageRowsSelectedHandler()} /> ), cell: ({ row }) => ( <input type="checkbox" aria-label={`Zaznacz wiersz ${row.id}`} checked={row.getIsSelected()} onChange={row.getToggleSelectedHandler()} /> ),}
Code
import { type RowSelectionState } from '@tanstack/react-table'// W DataTable — dodaj row selection stateconst [rowSelection, setRowSelection] = useState<RowSelectionState>({})const table = useReactTable({ // ... state: { sorting, globalFilter, rowSelection }, onRowSelectionChange: setRowSelection, enableRowSelection: true, getRowId,})// Dostęp do zaznaczonych wierszyconst selectedRows = table.getFilteredSelectedRowModel().rows
Kilka porad, zanim ruszysz dalej:
Używaj stabilnych identyfikatorów. Stan selekcji opieraj na ID z bazy
danych, a nie na indeksach wierszy.
Pamiętaj o ograniczeniach strony. getToggleAllPageRowsSelectedHandler
zaznacza tylko bieżącą stronę, a getFilteredSelectedRowModel() zna
wyłącznie aktualnie przekazane dane.
Weryfikuj uprawnienia na serwerze. Operacje masowe wykonuj po stronie
backendu i zawsze sprawdzaj uprawnienia dla każdego ID; zaznaczenie w
interfejsie to nie autoryzacja.
Zadbaj o stan indeterminate. w przypadku częściowego zaznaczenia ustaw
właściwość DOM indeterminate na checkboxie, ponieważ aria-checked="mixed"
obsługują tylko technologie asystujące.
Server-side table: paginacja z URL w Next.js
Przy dużych albo drogich zbiorach danych TanStack Table nadal zarządza stanem i UI, ale sortowanie, filtrowanie i paginacja dzieją się po stronie bazy. Pamiętaj, by nigdy nie mieszać klientowego sortowania z serwerową paginacją, bo nie posortujesz całego zbioru, ale tylko aktualnie pobraną stronę.
Najpierw parsuj searchParams bezpośrednio w komponencie serwerowym. Pamiętaj, aby parametry odpowiadające za sortowanie zawsze weryfikować za pomocą białej listy, zamiast bezwzględnie przekazywać wartości z URL do klauzuli orderBy.
Po stronie klienta stan tabeli jest w pełni kontrolowany przez parametry w adresie URL. Flagi manualPagination, manualSorting oraz manualFiltering informują TanStack Table, że przekazane dane zostały już odpowiednio przefiltrowane, posortowane i podzielone na strony.
Lokalny stan filterInput zapewnia natychmiastową responsywność interfejsu bez czekania na odpowiedź serwera, a mechanizm debounce skutecznie ogranicza liczbę zapytań nawigacyjnych. Zastosowanie router.replace zapobiega zaśmiecaniu historii przeglądarki każdą zmianą filtrów czy sortowania, z kolei po router.push sięgaj tylko wtedy, gdy celowo chcesz zachować każdy krok w historii. Przechowywanie stanu bezpośrednio w URL pozwala natomiast na odtworzenie dokładnego widoku po odświeżeniu strony oraz ułatwia dzielenie się bezpośrednim linkiem.
Stabilność i wydajność zapytań
Przy paginacji offsetowej dodaj do sortowania unikalny klucz rozstrzygający remisy, na przykład id. Bez niego rekordy o tej samej dacie lub kwocie mogą zmieniać kolejność między zapytaniami, co prowadzi do duplikatów albo pominięć między stronami. Indeksy w bazie powinny odpowiadać najczęstszym filtrom i sortowaniom, ale dobieraj je na podstawie planu zapytania, nie automatycznie dla każdej kolumny.
Duży skip staje się kosztowny przy odległych stronach. Jeśli użytkownik głównie przechodzi do następnej i poprzedniej partii, rozważ paginację kursorową opartą na tym samym stabilnym porządku. Pamiętaj też, że dokładne count() może być drogie na dużej, intensywnie filtrowanej tabeli — czasem lepsza jest informacja „jest następna strona” niż kosztowne obliczanie pełnej liczby stron.
Dostępność, responsywność i wirtualizacja
Sortowanie przypinaj do prawdziwego <button> wewnątrz <th>, a stan wystawiaj przez aria-sort, z kolei sam onClick na nagłówku nie zapewnia obsługi klawiatury. Trzeba dodać też podpis tabeli, etykietę wyszukiwarki, komunikat pustego wyniku i poziome przewijanie na wąskich ekranach. Przy bardzo wielu renderowanych wierszach sama paginacja nie zawsze wystarczy, ponieważ TanStack Table nie ma wbudowanej wirtualizacji, ale można połączyć go z TanStack Virtual albo react-window.
W testach sprawdź co najmniej zmianę kierunku sortowania, reset strony po filtrze, nieprawidłowe parametry URL, pusty wynik, nawigację wstecz/dalej oraz obsługę nagłówków klawiaturą. W trybie serwerowym szczególnie ważny jest test, że backend i UI obsługują dokładnie ten sam zestaw kolumn sortowalnych.
Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
TanStack Table jest headless i bardzo lekki, nie narzuca żadnych stylów, dając pełną kontrolę nad wyglądem. Jest to idealne rozwiązanie dla customowego UI w Tailwind. AG Grid to rozwiązanie klasy enterprise z mnóstwem wbudowanych funkcji i własnymi stylami, ale znacznie cięższe. MUI DataGrid wpisuje się w Material Design i jest wygodny, jeśli i tak używasz MUI. Dla projektów z własnym design systemem i Tailwindem naturalnym wyborem jest TanStack Table.
Czy mogę eksportować dane tabeli do CSV?
Tak. TanStack Table daje dostęp do aktualnie przefiltrowanych i posortowanych wierszy przed paginacją przez table.getPrePaginationRowModel().rows. getFilteredRowModel() uwzględnia filtry, ale jeszcze nie sortowanie. getRowModel() zwraca natomiast tylko aktualną stronę. Przy tabeli serwerowej modele TanStack znają wyłącznie pobrany wycinek, więc eksport całego wyniku musi wykonać osobny endpoint z tymi samymi filtrami, autoryzacją i limitem rozmiaru.
Jak obsłużyć tabele na urządzeniach mobilnych?
Są dwa dobre i sprawdzone podejścia. Pierwsze to poziomy scroll, czyli owijasz tabelę w kontener z overflow-x-auto, a dzięki temu na wąskim ekranie można ją przewijać w bok. Drugie, bardziej dopracowane, to zamiana tabeli na listę kart na małych ekranach, gdzie każdy wiersz staje się osobną kartą. TanStack Table wspiera też sterowanie widocznością kolumn, więc możesz po prostu ukryć mniej istotne kolumny na mobile.
Kiedy przejść z paginacji klienckiej na serwerową?
Nie ma uniwersalnego progu liczby rekordów. TanStack Table potrafi sprawnie przetwarzać tysiące prostych wierszy, ale decyzję podejmuj na podstawie rozmiaru odpowiedzi, liczby i złożoności kolumn, kosztu zapytania, pamięci urządzeń docelowych oraz czasu sortowania i filtrowania. Gdy nie chcesz pobierać całego zbioru albo potrzebujesz globalnie spójnych wyników, przejdź na tryb serwerowy ze searchParams.
Dlaczego dane pobierać w Server Component, a tabelę renderować w Client Component?
Bo to najlepiej wykorzystuje model App Routera. Server Component pobiera dane bezpośrednio z bazy bez wysyłania kodu zapytań do przeglądarki i bez dodatkowego round-tripu API. Sama tabela wymaga jednak interaktywności (sortowania, filtrowania, klikania), więc musi być Client Componentem z 'use client'. Do klienta przekazuj jednak wyłącznie serializowalne DTO. Definicje kolumn zawierają funkcje renderujące, dlatego importuj je w komponencie klienckim, zamiast przekazywać jako props z Server Component.
O autorze
Maciej Sala
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.
Data fetching , czyli sztuka sensownego pobierania danych, to bodajże najstarszy problem w świecie Reacta. Napisanie podstawowego useEffect wraz z fetch jest proste, ale dopisanie do niego logiki, która poprawnie obsłuży cache, usunie zdublowane zapytania, bezbłędnie zadba o powtórzenia w razie błędów retry i elegancko wybroni się przed nadpisywaniem starych wyników race conditions ... to już grubszy temat.
Maciej Sala
Founder StriveLab
Użytkownik ustawia trzy filtry, znajduje idealny wynik, wysyła link koledze — a tamten widzi pustą wyszukiwarkę. Bo filtry siedziały w useState i zginęły poza jego przeglądarką. URL state rozwiązuje to u źródła: cały stan filtrów żyje w adresie, więc przetrwa odświeżenie, udostępnienie i przycisk „wstecz”, a do tego jest czytelny dla Server Components. Pokazuję, jak zbudować taką wyszukiwarkę — z debounce , skeletonami, walidacją parametrów i SEO.
Maciej Sala
Founder StriveLab
Paginacja wydaje się prostym tematem, dopóki nie trzeba pogodzić trzech rzeczy naraz: dobrego UX, wydajności zapytań do bazy i tego, żeby Google poprawnie zaindeksował wszystkie strony. W Next.js App Router wybieramy, czy używamy numerycznej paginacji w URL czy infinite scroll, offset czy cursor po stronie bazy, adres kanoniczny czy notFound dla błędnych adresów. W tym artykule przechodzę przez warianty z kodem i rekomendacjami.