We use cookies to enhance your experience on the site
CodeWorlds

Next.js z TanStack Table - Zaawansowane tabele danych

In Quantum Metropolis, where data flows like streams of quantum information, you will discover TanStack Table - the most powerful library for building advanced tables in React. Combined with Next.js, it lets you build interactive, performant interfaces for managing large data sets.

What is TanStack Table?

TanStack Table (previously React Table) is a headless library for building tables that offers:

  • Headless UI - full control over look and feel
  • TypeScript-first - full type support
  • Framework agnostic - works with React, Vue, Svelte
  • Feature-rich - sorting, filtering, pagination, grouping
  • Performance - virtualization and optimizations
  • Extensible - a rich plugin system

Zalety TanStack Table

  • Zero dependencies - nie wymaga dodatkowych bibliotek
  • Tree-shakable - tylko potrzebne funkcje w bundlu
  • Server-side support - idealny dla SSR/SSG
  • Accessibility - wbudowane wsparcie a11y
  • Mobile-friendly - responsywne tabele

Instalacja i podstawowa konfiguracja

Instalacja

1# Zainstaluj TanStack Table
2npm install @tanstack/react-table
3
4# Dla stylowania (opcjonalne)
5npm install @tanstack/table-core

Podstawowa tabela

1// types/User.ts
2export interface User {
3  id: string;
4  name: string;
5  email: string;
6  role: 'admin' | 'user' | 'moderator';
7  status: 'active' | 'inactive' | 'pending';
8  createdAt: string;
9  lastLogin?: string;
10}
1// components/UsersTable.tsx
2'use client';
3
4import {
5  createColumnHelper,
6  flexRender,
7  getCoreRowModel,
8  useReactTable,
9} from '@tanstack/react-table';
10import { User } from '@/types/User';
11
12const columnHelper = createColumnHelper<User>();
13
14const columns = [
15  columnHelper.accessor('name', {
16    header: 'Username',
17    cell: (info) => info.getValue(),
18  }),
19  columnHelper.accessor('email', {
20    header: 'Email',
21    cell: (info) => (
22      <a href={`mailto:${info.getValue()}`} className="text-blue-600 hover:underline">
23        {info.getValue()}
24      </a>
25    ),
26  }),
27  columnHelper.accessor('role', {
28    header: 'Role',
29    cell: (info) => {
30      const role = info.getValue();
31      const roleColors = {
32        admin: 'bg-red-100 text-red-800',
33        moderator: 'bg-yellow-100 text-yellow-800',
34        user: 'bg-green-100 text-green-800',
35      };
36      
37      return (
38        <span className={`px-2 py-1 rounded-full text-xs font-medium ${roleColors[role]}`}>
39          {role}
40        </span>
41      );
42    },
43  }),
44  columnHelper.accessor('status', {
45    header: 'Status',
46    cell: (info) => {
47      const status = info.getValue();
48      const statusColors = {
49        active: 'bg-green-100 text-green-800',
50        inactive: 'bg-gray-100 text-gray-800',
51        pending: 'bg-yellow-100 text-yellow-800',
52      };
53      
54      return (
55        <span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColors[status]}`}>
56          {status}
57        </span>
58      );
59    },
60  }),
61  columnHelper.accessor('createdAt', {
62    header: 'Created at',
63    cell: (info) => new Date(info.getValue()).toLocaleDateString('en-US'),
64  }),
65];
66
67interface UsersTableProps {
68  data: User[];
69}
70
71export default function UsersTable({ data }: UsersTableProps) {
72  const table = useReactTable({
73    data,
74    columns,
75    getCoreRowModel: getCoreRowModel(),
76  });
77
78  return (
79    <div className="overflow-x-auto">
80      <table className="min-w-full bg-white border border-gray-200">
81        <thead className="bg-gray-50">
82          {table.getHeaderGroups().map((headerGroup) => (
83            <tr key={headerGroup.id}>
84              {headerGroup.headers.map((header) => (
85                <th
86                  key={header.id}
87                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
88                >
89                  {header.isPlaceholder
90                    ? null
91                    : flexRender(header.column.columnDef.header, header.getContext())}
92                </th>
93              ))}
94            </tr>
95          ))}
96        </thead>
97        <tbody className="bg-white divide-y divide-gray-200">
98          {table.getRowModel().rows.map((row) => (
99            <tr key={row.id} className="hover:bg-gray-50">
100              {row.getVisibleCells().map((cell) => (
101                <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
102                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
103                </td>
104              ))}
105            </tr>
106          ))}
107        </tbody>
108      </table>
109    </div>
110  );
111}

Sortowanie i filtrowanie

Sortowanie

1// components/SortableUsersTable.tsx
2'use client';
3
4import {
5  createColumnHelper,
6  flexRender,
7  getCoreRowModel,
8  getSortedRowModel,
9  SortingState,
10  useReactTable,
11} from '@tanstack/react-table';
12import { useState } from 'react';
13import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
14import { User } from '@/types/User';
15
16const columnHelper = createColumnHelper<User>();
17
18const columns = [
19  columnHelper.accessor('name', {
20    header: 'Username',
21    enableSorting: true,
22  }),
23  columnHelper.accessor('email', {
24    header: 'Email',
25    enableSorting: true,
26  }),
27  columnHelper.accessor('role', {
28    header: 'Role',
29    enableSorting: true,
30  }),
31  columnHelper.accessor('status', {
32    header: 'Status',
33    enableSorting: true,
34  }),
35  columnHelper.accessor('createdAt', {
36    header: 'Data utworzenia',
37    enableSorting: true,
38    sortingFn: 'datetime',
39    cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
40  }),
41];
42
43interface SortableUsersTableProps {
44  data: User[];
45}
46
47export default function SortableUsersTable({ data }: SortableUsersTableProps) {
48  const [sorting, setSorting] = useState<SortingState>([]);
49
50  const table = useReactTable({
51    data,
52    columns,
53    state: {
54      sorting,
55    },
56    onSortingChange: setSorting,
57    getCoreRowModel: getCoreRowModel(),
58    getSortedRowModel: getSortedRowModel(),
59  });
60
61  return (
62    <div className="overflow-x-auto">
63      <table className="min-w-full bg-white border border-gray-200">
64        <thead className="bg-gray-50">
65          {table.getHeaderGroups().map((headerGroup) => (
66            <tr key={headerGroup.id}>
67              {headerGroup.headers.map((header) => (
68                <th
69                  key={header.id}
70                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
71                >
72                  <div
73                    className={
74                      header.column.getCanSort()
75                        ? 'flex items-center cursor-pointer select-none hover:text-gray-700'
76                        : ''
77                    }
78                    onClick={header.column.getToggleSortingHandler()}
79                  >
80                    {flexRender(header.column.columnDef.header, header.getContext())}
81                    {header.column.getCanSort() && (
82                      <span className="ml-1">
83                        {header.column.getIsSorted() === 'asc' ? (
84                          <ChevronUpIcon className="w-4 h-4" />
85                        ) : header.column.getIsSorted() === 'desc' ? (
86                          <ChevronDownIcon className="w-4 h-4" />
87                        ) : (
88                          <div className="w-4 h-4" />
89                        )}
90                      </span>
91                    )}
92                  </div>
93                </th>
94              ))}
95            </tr>
96          ))}
97        </thead>
98        <tbody className="bg-white divide-y divide-gray-200">
99          {table.getRowModel().rows.map((row) => (
100            <tr key={row.id} className="hover:bg-gray-50">
101              {row.getVisibleCells().map((cell) => (
102                <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
103                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
104                </td>
105              ))}
106            </tr>
107          ))}
108        </tbody>
109      </table>
110    </div>
111  );
112}

Filtrowanie

1// components/FilterableUsersTable.tsx
2'use client';
3
4import {
5  createColumnHelper,
6  flexRender,
7  getCoreRowModel,
8  getFilteredRowModel,
9  getSortedRowModel,
10  SortingState,
11  ColumnFiltersState,
12  useReactTable,
13  GlobalFilterTableState,
14} from '@tanstack/react-table';
15import { useState } from 'react';
16import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
17import { User } from '@/types/User';
18
19// Global filter function
20function globalFilterFn(row: any, columnId: string, value: string) {
21  const search = value.toLowerCase();
22  return (
23    row.getValue('name')?.toLowerCase().includes(search) ||
24    row.getValue('email')?.toLowerCase().includes(search) ||
25    row.getValue('role')?.toLowerCase().includes(search)
26  );
27}
28
29const columnHelper = createColumnHelper<User>();
30
31const columns = [
32  columnHelper.accessor('name', {
33    header: 'Username',
34    enableColumnFilter: true,
35    filterFn: 'includesString',
36  }),
37  columnHelper.accessor('email', {
38    header: 'Email',
39    enableColumnFilter: true,
40  }),
41  columnHelper.accessor('role', {
42    header: 'Role',
43    enableColumnFilter: true,
44    filterFn: 'equals',
45  }),
46  columnHelper.accessor('status', {
47    header: 'Status',
48    enableColumnFilter: true,
49    filterFn: 'equals',
50  }),
51  columnHelper.accessor('createdAt', {
52    header: 'Data utworzenia',
53    enableColumnFilter: false,
54    cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
55  }),
56];
57
58interface FilterableUsersTableProps {
59  data: User[];
60}
61
62export default function FilterableUsersTable({ data }: FilterableUsersTableProps) {
63  const [sorting, setSorting] = useState<SortingState>([]);
64  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
65  const [globalFilter, setGlobalFilter] = useState('');
66
67  const table = useReactTable({
68    data,
69    columns,
70    state: {
71      sorting,
72      columnFilters,
73      globalFilter,
74    },
75    onSortingChange: setSorting,
76    onColumnFiltersChange: setColumnFilters,
77    onGlobalFilterChange: setGlobalFilter,
78    globalFilterFn,
79    getCoreRowModel: getCoreRowModel(),
80    getSortedRowModel: getSortedRowModel(),
81    getFilteredRowModel: getFilteredRowModel(),
82  });
83
84  return (
85    <div className="space-y-4">
86      {/* Global Search */}
87      <div className="flex items-center space-x-4">
88        <div className="relative flex-1 max-w-sm">
89          <MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
90          <input
91            value={globalFilter ?? ''}
92            onChange={(e) => setGlobalFilter(e.target.value)}
93            className="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full"
94          />
95        </div>
96        
97        {/* Roles Filter */}
98        <select
99          value={(table.getColumn('role')?.getFilterValue() as string) ?? ''}
100          onChange={(e) =>
101            table.getColumn('role')?.setFilterValue(e.target.value || undefined)
102          }
103          className="px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
104        >
105          <option value="">Wszystkie role</option>
106          <option value="admin">Admin</option>
107          <option value="moderator">Moderator</option>
108          <option value="user">User</option>
109        </select>
110
111        {/* Status Filter */}
112        <select
113          value={(table.getColumn('status')?.getFilterValue() as string) ?? ''}
114          onChange={(e) =>
115            table.getColumn('status')?.setFilterValue(e.target.value || undefined)
116          }
117          className="px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
118        >
119          <option value="">All statuses</option>
120          <option value="active">Active</option>
121          <option value="inactive">Inactive</option>
122          <option value="pending">Pending</option>
123        </select>
124      </div>
125
126      {/* Results Info */}
127      <div className="text-sm text-gray-600">
128        Showing {table.getFilteredRowModel().rows.length} of {data.length} users
129      </div>
130
131      {/* Table */}
132      <div className="overflow-x-auto">
133        <table className="min-w-full bg-white border border-gray-200">
134          <thead className="bg-gray-50">
135            {table.getHeaderGroups().map((headerGroup) => (
136              <tr key={headerGroup.id}>
137                {headerGroup.headers.map((header) => (
138                  <th
139                    key={header.id}
140                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
141                  >
142                    <div
143                      className={
144                        header.column.getCanSort()
145                          ? 'flex items-center cursor-pointer select-none hover:text-gray-700'
146                          : ''
147                      }
148                      onClick={header.column.getToggleSortingHandler()}
149                    >
150                      {flexRender(header.column.columnDef.header, header.getContext())}
151                    </div>
152                  </th>
153                ))}
154              </tr>
155            ))}
156          </thead>
157          <tbody className="bg-white divide-y divide-gray-200">
158            {table.getFilteredRowModel().rows.map((row) => (
159              <tr key={row.id} className="hover:bg-gray-50">
160                {row.getVisibleCells().map((cell) => (
161                  <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
162                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
163                  </td>
164                ))}
165              </tr>
166            ))}
167          </tbody>
168        </table>
169      </div>
170    </div>
171  );
172}

Pagination

1// components/PaginatedUsersTable.tsx
2'use client';
3
4import {
5  createColumnHelper,
6  flexRender,
7  getCoreRowModel,
8  getPaginationRowModel,
9  getSortedRowModel,
10  getFilteredRowModel,
11  PaginationState,
12  useReactTable,
13} from '@tanstack/react-table';
14import { useState } from 'react';
15import { 
16  ChevronLeftIcon, 
17  ChevronRightIcon,
18  ChevronDoubleLeftIcon,
19  ChevronDoubleRightIcon 
20} from '@heroicons/react/24/outline';
21import { User } from '@/types/User';
22
23const columnHelper = createColumnHelper<User>();
24
25const columns = [
26  columnHelper.accessor('name', {
27    header: 'Username',
28  }),
29  columnHelper.accessor('email', {
30    header: 'Email',
31  }),
32  columnHelper.accessor('role', {
33    header: 'Role',
34    cell: (info) => {
35      const role = info.getValue();
36      return (
37        <span className="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
38          {role}
39        </span>
40      );
41    },
42  }),
43  columnHelper.accessor('status', {
44    header: 'Status',
45    cell: (info) => {
46      const status = info.getValue();
47      const colors = {
48        active: 'bg-green-100 text-green-800',
49        inactive: 'bg-gray-100 text-gray-800',
50        pending: 'bg-yellow-100 text-yellow-800',
51      };
52      
53      return (
54        <span className={`px-2 py-1 rounded-full text-xs font-medium ${colors[status]}`}>
55          {status}
56        </span>
57      );
58    },
59  }),
60  columnHelper.accessor('createdAt', {
61    header: 'Created at',
62    cell: (info) => new Date(info.getValue()).toLocaleDateString('en-US'),
63  }),
64];
65
66interface PaginatedUsersTableProps {
67  data: User[];
68}
69
70export default function PaginatedUsersTable({ data }: PaginatedUsersTableProps) {
71  const [pagination, setPagination] = useState<PaginationState>({
72    pageIndex: 0,
73    pageSize: 10,
74  });
75
76  const table = useReactTable({
77    data,
78    columns,
79    state: {
80      pagination,
81    },
82    onPaginationChange: setPagination,
83    getCoreRowModel: getCoreRowModel(),
84    getSortedRowModel: getSortedRowModel(),
85    getFilteredRowModel: getFilteredRowModel(),
86    getPaginationRowModel: getPaginationRowModel(),
87  });
88
89  return (
90    <div className="space-y-4">
91      {/* Table */}
92      <div className="overflow-x-auto">
93        <table className="min-w-full bg-white border border-gray-200">
94          <thead className="bg-gray-50">
95            {table.getHeaderGroups().map((headerGroup) => (
96              <tr key={headerGroup.id}>
97                {headerGroup.headers.map((header) => (
98                  <th
99                    key={header.id}
100                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
101                  >
102                    {flexRender(header.column.columnDef.header, header.getContext())}
103                  </th>
104                ))}
105              </tr>
106            ))}
107          </thead>
108          <tbody className="bg-white divide-y divide-gray-200">
109            {table.getRowModel().rows.map((row) => (
110              <tr key={row.id} className="hover:bg-gray-50">
111                {row.getVisibleCells().map((cell) => (
112                  <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
113                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
114                  </td>
115                ))}
116              </tr>
117            ))}
118          </tbody>
119        </table>
120      </div>
121
122      {/* Pagination */}
123      <div className="flex items-center justify-between">
124        <div className="flex items-center space-x-2">
125          <span className="text-sm text-gray-700">
126            Showing{' '}
127            <span className="font-medium">
128              {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
129            </span>{' '}
130            -{' '}
131            <span className="font-medium">
132              {Math.min(
133                (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
134                table.getFilteredRowModel().rows.length
135              )}
136            </span>{' '}
137            of{' '}
138            <span className="font-medium">{table.getFilteredRowModel().rows.length}</span> results
139          </span>
140          
141          <select
142            value={table.getState().pagination.pageSize}
143            onChange={(e) => {
144              table.setPageSize(Number(e.target.value));
145            }}
146            className="px-3 py-1 border border-gray-300 rounded text-sm"
147          >
148            {[10, 20, 30, 40, 50].map((pageSize) => (
149              <option key={pageSize} value={pageSize}>
150                {pageSize} na page
151              </option>
152            ))}
153          </select>
154        </div>
155
156        <div className="flex items-center space-x-2">
157          <button
158            onClick={() => table.setPageIndex(0)}
159            disabled={!table.getCanPreviousPage()}
160            className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
161          >
162            <ChevronDoubleLeftIcon className="w-4 h-4" />
163          </button>
164          
165          <button
166            onClick={() => table.previousPage()}
167            disabled={!table.getCanPreviousPage()}
168            className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
169          >
170            <ChevronLeftIcon className="w-4 h-4" />
171          </button>
172
173          <div className="flex items-center space-x-1">
174            <span className="text-sm text-gray-700">Strona</span>
175            <input
176              type="number"
177              value={table.getState().pagination.pageIndex + 1}
178              onChange={(e) => {
179                const page = e.target.value ? Number(e.target.value) - 1 : 0;
180                table.setPageIndex(page);
181              }}
182              className="w-16 px-2 py-1 border border-gray-300 rounded text-sm text-center"
183              min={1}
184              max={table.getPageCount()}
185            />
186            <span className="text-sm text-gray-700">z {table.getPageCount()}</span>
187          </div>
188
189          <button
190            onClick={() => table.nextPage()}
191            disabled={!table.getCanNextPage()}
192            className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
193          >
194            <ChevronRightIcon className="w-4 h-4" />
195          </button>
196          
197          <button
198            onClick={() => table.setPageIndex(table.getPageCount() - 1)}
199            disabled={!table.getCanNextPage()}
200            className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
201          >
202            <ChevronDoubleRightIcon className="w-4 h-4" />
203          </button>
204        </div>
205      </div>
206    </div>
207  );
208}

Selekcja wierszy

1// components/SelectableUsersTable.tsx
2'use client';
3
4import {
5  createColumnHelper,
6  flexRender,
7  getCoreRowModel,
8  useReactTable,
9  RowSelectionState,
10} from '@tanstack/react-table';
11import { useState } from 'react';
12import { TrashIcon, UserGroupIcon } from '@heroicons/react/24/outline';
13import { User } from '@/types/User';
14
15const columnHelper = createColumnHelper<User>();
16
17interface SelectableUsersTableProps {
18  data: User[];
19  onDeleteSelected?: (selectedUsers: User[]) => void;
20  onBulkAction?: (action: string, selectedUsers: User[]) => void;
21}
22
23export default function SelectableUsersTable({ 
24  data, 
25  onDeleteSelected,
26  onBulkAction 
27}: SelectableUsersTableProps) {
28  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
29
30  const columns = [
31    // Selection column
32    columnHelper.display({
33      id: 'select',
34      header: ({ table }) => (
35        <input
36          type="checkbox"
37          checked={table.getIsAllRowsSelected()}
38          onChange={table.getToggleAllRowsSelectedHandler()}
39          className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
40        />
41      ),
42      cell: ({ row }) => (
43        <input
44          type="checkbox"
45          checked={row.getIsSelected()}
46          onChange={row.getToggleSelectedHandler()}
47          className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
48        />
49      ),
50    }),
51    columnHelper.accessor('name', {
52      header: 'Username',
53    }),
54    columnHelper.accessor('email', {
55      header: 'Email',
56    }),
57    columnHelper.accessor('role', {
58      header: 'Role',
59    }),
60    columnHelper.accessor('status', {
61      header: 'Status',
62    }),
63    // Actions column
64    columnHelper.display({
65      id: 'actions',
66      header: 'Akcje',
67      cell: ({ row }) => (
68        <div className="flex space-x-2">
69          <button
70            onClick={() => console.log('Edit user:', row.original)}
71            className="text-blue-600 hover:text-blue-800 text-sm"
72          >
73            Edytuj
74          </button>
75          <button
76            onClick={() => console.log('Delete user:', row.original)}
77            className="text-red-600 hover:text-red-800 text-sm"
78          >
79            Remove
80          </button>
81        </div>
82      ),
83    }),
84  ];
85
86  const table = useReactTable({
87    data,
88    columns,
89    state: {
90      rowSelection,
91    },
92    onRowSelectionChange: setRowSelection,
93    getCoreRowModel: getCoreRowModel(),
94    enableRowSelection: true,
95  });
96
97  const selectedUsers = table.getFilteredSelectedRowModel().rows.map(row => row.original);
98
99  return (
100    <div className="space-y-4">
101      {/* Bulk Actions */}
102      {Object.keys(rowSelection).length > 0 && (
103        <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
104          <div className="flex items-center justify-between">
105            <div className="flex items-center space-x-2">
106              <UserGroupIcon className="w-5 h-5 text-blue-600" />
107              <span className="text-sm font-medium text-blue-900">
108                Zaznaczono {selectedUsers.length} users
109              </span>
110            </div>
111            
112            <div className="flex space-x-2">
113              <button
114                onClick={() => onBulkAction?.('activate', selectedUsers)}
115                className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700"
116              >
117                Aktywuj
118              </button>
119              <button
120                onClick={() => onBulkAction?.('deactivate', selectedUsers)}
121                className="px-3 py-1 bg-yellow-600 text-white text-sm rounded hover:bg-yellow-700"
122              >
123                Dezaktywuj
124              </button>
125              <button
126                onClick={() => onDeleteSelected?.(selectedUsers)}
127                className="px-3 py-1 bg-red-600 text-white text-sm rounded hover:bg-red-700 flex items-center space-x-1"
128              >
129                <TrashIcon className="w-4 h-4" />
130                <span>Remove</span>
131              </button>
132            </div>
133          </div>
134        </div>
135      )}
136
137      {/* Table */}
138      <div className="overflow-x-auto">
139        <table className="min-w-full bg-white border border-gray-200">
140          <thead className="bg-gray-50">
141            {table.getHeaderGroups().map((headerGroup) => (
142              <tr key={headerGroup.id}>
143                {headerGroup.headers.map((header) => (
144                  <th
145                    key={header.id}
146                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
147                  >
148                    {flexRender(header.column.columnDef.header, header.getContext())}
149                  </th>
150                ))}
151              </tr>
152            ))}
153          </thead>
154          <tbody className="bg-white divide-y divide-gray-200">
155            {table.getRowModel().rows.map((row) => (
156              <tr 
157                key={row.id} 
158                className={`hover:bg-gray-50 ${row.getIsSelected() ? 'bg-blue-50' : ''}`}
159              >
160                {row.getVisibleCells().map((cell) => (
161                  <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
162                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
163                  </td>
164                ))}
165              </tr>
166            ))}
167          </tbody>
168        </table>
169      </div>
170    </div>
171  );
172}

Server-side data fetching

1// hooks/useUsersData.ts
2import { useQuery } from '@tanstack/react-query';
3import { useState } from 'react';
4
5interface UseUsersDataParams {
6  page: number;
7  pageSize: number;
8  sorting?: Array<{ id: string; desc: boolean }>;
9  filters?: Array<{ id: string; value: any }>;
10  globalFilter?: string;
11}
12
13interface UsersResponse {
14  data: User[];
15  totalCount: number;
16  pageCount: number;
17}
18
19export function useUsersData(params: UseUsersDataParams) {
20  return useQuery({
21    queryKey: ['users', params],
22    queryFn: async (): Promise<UsersResponse> => {
23      const searchParams = new URLSearchParams({
24        page: params.page.toString(),
25        pageSize: params.pageSize.toString(),
26        ...(params.globalFilter && { search: params.globalFilter }),
27        ...(params.sorting?.length && { 
28          sort: JSON.stringify(params.sorting) 
29        }),
30        ...(params.filters?.length && { 
31          filters: JSON.stringify(params.filters) 
32        }),
33      });
34
35      const response = await fetch(`/api/users?${searchParams}`);
36      
37      if (!response.ok) {
38        throw new Error('Failed to fetch users');
39      }
40      
41      return response.json();
42    },
43    keepPreviousData: true,
44  });
45}
1// components/ServerSideUsersTable.tsx
2'use client';
3
4import {
5  createColumnHelper,
6  flexRender,
7  getCoreRowModel,
8  useReactTable,
9  PaginationState,
10  SortingState,
11  ColumnFiltersState,
12} from '@tanstack/react-table';
13import { useState } from 'react';
14import { useUsersData } from '@/hooks/useUsersData';
15import { User } from '@/types/User';
16
17const columnHelper = createColumnHelper<User>();
18
19const columns = [
20  columnHelper.accessor('name', {
21    header: 'Username',
22    enableSorting: true,
23  }),
24  columnHelper.accessor('email', {
25    header: 'Email',
26    enableSorting: true,
27  }),
28  columnHelper.accessor('role', {
29    header: 'Role',
30    enableSorting: true,
31  }),
32  columnHelper.accessor('status', {
33    header: 'Status',
34    enableSorting: true,
35  }),
36  columnHelper.accessor('createdAt', {
37    header: 'Data utworzenia',
38    enableSorting: true,
39    cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
40  }),
41];
42
43export default function ServerSideUsersTable() {
44  const [pagination, setPagination] = useState<PaginationState>({
45    pageIndex: 0,
46    pageSize: 10,
47  });
48  const [sorting, setSorting] = useState<SortingState>([]);
49  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
50  const [globalFilter, setGlobalFilter] = useState('');
51
52  const { data, andsLoading, error } = useUsersData({
53    page: pagination.pageIndex,
54    pageSize: pagination.pageSize,
55    sorting,
56    filters: columnFilters,
57    globalFilter,
58  });
59
60  const table = useReactTable({
61    data: data?.data ?? [],
62    columns,
63    pageCount: data?.pageCount ?? -1,
64    state: {
65      pagination,
66      sorting,
67      columnFilters,
68      globalFilter,
69    },
70    onPaginationChange: setPagination,
71    onSortingChange: setSorting,
72    onColumnFiltersChange: setColumnFilters,
73    onGlobalFilterChange: setGlobalFilter,
74    getCoreRowModel: getCoreRowModel(),
75    manualPagination: true,
76    manualSorting: true,
77    manualFiltering: true,
78  });
79
80  if (isLoading) {
81    return (
82      <div className="flex items-center justify-center h-64">
83        <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
84      </div>
85    );
86  }
87
88  if (error) {
89    return (
90      <div className="text-center text-red-600 p-8">
91        An error occurred while loading data
92      </div>
93    );
94  }
95
96  return (
97    <div className="space-y-4">
98      {/* Search */}
99      <div className="flex items-center space-x-4">
100        <input
101          value={globalFilter ?? ''}
102          onChange={(e) => setGlobalFilter(e.target.value)}
103          className="px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 flex-1 max-w-sm"
104        />
105      </div>
106
107      {/* Table */}
108      <div className="overflow-x-auto">
109        <table className="min-w-full bg-white border border-gray-200">
110          <thead className="bg-gray-50">
111            {table.getHeaderGroups().map((headerGroup) => (
112              <tr key={headerGroup.id}>
113                {headerGroup.headers.map((header) => (
114                  <th
115                    key={header.id}
116                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
117                  >
118                    <div
119                      className={
120                        header.column.getCanSort()
121                          ? 'flex items-center cursor-pointer select-none hover:text-gray-700'
122                          : ''
123                      }
124                      onClick={header.column.getToggleSortingHandler()}
125                    >
126                      {flexRender(header.column.columnDef.header, header.getContext())}
127                    </div>
128                  </th>
129                ))}
130              </tr>
131            ))}
132          </thead>
133          <tbody className="bg-white divide-y divide-gray-200">
134            {table.getRowModel().rows.map((row) => (
135              <tr key={row.id} className="hover:bg-gray-50">
136                {row.getVisibleCells().map((cell) => (
137                  <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
138                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
139                  </td>
140                ))}
141              </tr>
142            ))}
143          </tbody>
144        </table>
145      </div>
146
147      {/* Pagination */}
148      <div className="flex items-center justify-between">
149        <div className="text-sm text-gray-700">
150          Showing {data?.data.length ?? 0} of {data?.totalCount ?? 0} users
151        </div>
152
153        <div className="flex items-center space-x-2">
154          <button
155            onClick={() => table.setPageIndex(0)}
156            disabled={!table.getCanPreviousPage()}
157            className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
158          >
159            First
160          </button>
161
162          <button
163            onClick={() => table.previousPage()}
164            disabled={!table.getCanPreviousPage()}
165            className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
166          >
167            Previous
168          </button>
169
170          <span className="text-sm">
171            Page {table.getState().pagination.pageIndex + 1} of{' '}
172            {table.getPageCount()}
173          </span>
174
175          <button
176            onClick={() => table.nextPage()}
177            disabled={!table.getCanNextPage()}
178            className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
179          >
180            Next
181          </button>
182
183          <button
184            onClick={() => table.setPageIndex(table.getPageCount() - 1)}
185            disabled={!table.getCanNextPage()}
186            className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
187          >
188            Last
189          </button>
190        </div>
191      </div>
192    </div>
193  );
194}

API Routes for server-side table

1// app/api/users/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { User } from '@/types/User';
4
5// Simulated data
6const mockUsers: User[] = [
7  // ... user data
8];
9
10export async function GET(request: NextRequest) {
11  const searchParams = request.nextUrl.searchParams;
12  
13  const page = parseInt(searchParams.get('page') || '0');
14  const pageSize = parseInt(searchParams.get('pageSize') || '10');
15  const search = searchParams.get('search') || '';
16  const sortParam = searchParams.get('sort');
17  const filtersParam = searchParams.get('filters');
18
19  let filteredUsers = [...mockUsers];
20
21  // Global search
22  if (search) {
23    filteredUsers = filteredUsers.filter(user =>
24      user.name.toLowerCase().includes(search.toLowerCase()) ||
25      user.email.toLowerCase().includes(search.toLowerCase()) ||
26      user.role.toLowerCase().includes(search.toLowerCase())
27    );
28  }
29
30  // Column filters
31  if (filtersParam) {
32    const filters = JSON.parse(filtersParam);
33    filters.forEach((filter: { id: string; value: any }) => {
34      filteredUsers = filteredUsers.filter(user => {
35        const value = user[filter.id as keyof User];
36        return value === filter.value;
37      });
38    });
39  }
40
41  // Sorting
42  if (sortParam) {
43    const sorting = JSON.parse(sortParam);
44    sorting.forEach((sort: { id: string; desc: boolean }) => {
45      filteredUsers.sort((a, b) => {
46        const aVal = a[sort.id as keyof User];
47        const bVal = b[sort.id as keyof User];
48        
49        if (aVal < bVal) return sort.desc ? 1 : -1;
50        if (aVal > bVal) return sort.desc ? -1 : 1;
51        return 0;
52      });
53    });
54  }
55
56  // Pagination
57  const totalCount = filteredUsers.length;
58  const pageCount = Math.ceil(totalCount / pageSize);
59  const startIndex = page * pageSize;
60  const endIndex = startIndex + pageSize;
61  const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
62
63  return NextResponse.json({
64    data: paginatedUsers,
65    totalCount,
66    pageCount,
67  });
68}

Responsive tables

1// components/ResponsiveUsersTable.tsx
2'use client';
3
4import { useState } from 'react';
5import { User } from '@/types/User';
6
7interface ResponsiveUsersTableProps {
8  data: User[];
9}
10
11export default function ResponsiveUsersTable({ data }: ResponsiveUsersTableProps) {
12  const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
13
14  const toggleRow = (userId: string) => {
15    const newExpanded = new Set(expandedRows);
16    if (newExpanded.has(userId)) {
17      newExpanded.delete(userId);
18    } else {
19      newExpanded.add(userId);
20    }
21    setExpandedRows(newExpanded);
22  };
23
24  return (
25    <div className="w-full">
26      {/* Desktop Table */}
27      <div className="hidden md:block overflow-x-auto">
28        <table className="min-w-full bg-white border border-gray-200">
29          {/* Standard desktop table */}
30        </table>
31      </div>
32
33      {/* Mobile Cards */}
34      <div className="md:hidden space-y-4">
35        {data.map((user) => (
36          <div key={user.id} className="bg-white border border-gray-200 rounded-lg shadow">
37            <div className="p-4">
38              <div className="flex items-center justify-between">
39                <div className="flex-1">
40                  <h3 className="font-medium text-gray-900">{user.name}</h3>
41                  <p className="text-sm text-gray-600">{user.email}</p>
42                </div>
43                
44                <button
45                  onClick={() => toggleRow(user.id)}
46                  className="ml-4 text-blue-600 text-sm"
47                >
48                  {expandedRows.has(user.id) ? 'Less' : 'More'}
49                </button>
50              </div>
51
52              {expandedRows.has(user.id) && (
53                <div className="mt-4 pt-4 border-t border-gray-200">
54                  <div className="grid grid-cols-2 gap-4 text-sm">
55                    <div>
56                      <span className="font-medium text-gray-700">Role:</span>
57                      <span className="ml-1">{user.role}</span>
58                    </div>
59                    <div>
60                      <span className="font-medium text-gray-700">Status:</span>
61                      <span className="ml-1">{user.status}</span>
62                    </div>
63                    <div className="col-span-2">
64                      <span className="font-medium text-gray-700">Created:</span>
65                      <span className="ml-1">
66                        {new Date(user.createdAt).toLocaleDateString('en-US')}
67                      </span>
68                    </div>
69                  </div>
70                </div>
71              )}
72            </div>
73          </div>
74        ))}
75      </div>
76    </div>
77  );
78}

Data export

1// hooks/useTableExport.ts
2import { User } from '@/types/User';
3
4export function useTableExport() {
5  const exportToCSV = (data: User[], filename = 'users.csv') => {
6    const headers = ['ID', 'Nazwa', 'Email', 'Rola', 'Status', 'Data utworzenia'];
7    const csvContent = [
8      headers.join(','),
9      ...data.map(user => [
10        user.id,
11        user.name,
12        user.email,
13        user.role,
14        user.status,
15        new Date(user.createdAt).toLocaleDateString('pl-PL')
16      ].join(','))
17    ].join('
18');
19
20    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
21    const link = document.createElement('a');
22    const url = URL.createObjectURL(blob);
23    link.setAttribute('href', url);
24    link.setAttribute('download', filename);
25    link.style.visibility = 'hidden';
26    document.body.appendChild(link);
27    link.click();
28    document.body.removeChild(link);
29  };
30
31  const exportToJSON = (data: User[], filename = 'users.json') => {
32    const jsonContent = JSON.stringify(data, null, 2);
33    const blob = new Blob([jsonContent], { type: 'application/json' });
34    const link = document.createElement('a');
35    const url = URL.createObjectURL(blob);
36    link.setAttribute('href', url);
37    link.setAttribute('download', filename);
38    link.style.visibility = 'hidden';
39    document.body.appendChild(link);
40    link.click();
41    document.body.removeChild(link);
42  };
43
44  return { exportToCSV, exportToJSON };
45}

Best practices

1. Performance optimization

1// ✅ Good - use memo for columns
2const columns = useMemo(() => [
3  // column definitions
4], []);
5
6// ✅ Good - memo for data
7const data = useMemo(() => users, [users]);

2. Accessibility

1// ✅ Good - aria labels
2<table role="table" aria-label="Users table">
3  <thead>
4    <tr role="row">
5      <th role="columnheader" aria-sort="ascending">
6        Name
7      </th>
8    </tr>
9  </thead>
10</table>

3. TypeScript integration

1// ✅ Good - strong typing
2const columnHelper = createColumnHelper<User>();
3
4const columns = [
5  columnHelper.accessor('name', {
6    // Full IntelliSense support
7  }),
8];

Summary

TanStack Table with Next.js forms a powerful combination for building advanced tables:

  1. Headless UI - full styling control
  2. TypeScript-first - type safety
  3. Feature-rich - sorting, filtering, pagination
  4. Performance - optimizations and virtualization
  5. Server-side support - ideal for large data sets

TanStack Table is the best library for building advanced, interactive tables in React applications.

Go to CodeWorlds