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.
TanStack Table (previously React Table) is a headless library for building tables that offers:
1# Zainstaluj TanStack Table
2npm install @tanstack/react-table
3
4# Dla stylowania (opcjonalne)
5npm install @tanstack/table-core1// 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}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}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}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}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}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}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}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}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}1// ✅ Good - use memo for columns
2const columns = useMemo(() => [
3 // column definitions
4], []);
5
6// ✅ Good - memo for data
7const data = useMemo(() => users, [users]);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>1// ✅ Good - strong typing
2const columnHelper = createColumnHelper<User>();
3
4const columns = [
5 columnHelper.accessor('name', {
6 // Full IntelliSense support
7 }),
8];TanStack Table with Next.js forms a powerful combination for building advanced tables:
TanStack Table is the best library for building advanced, interactive tables in React applications.