Skip to content

Instantly share code, notes, and snippets.

@devhammed
Last active November 27, 2025 09:29
Show Gist options
  • Select an option

  • Save devhammed/533562a87cba0eda6348fdad3d2e32a2 to your computer and use it in GitHub Desktop.

Select an option

Save devhammed/533562a87cba0eda6348fdad3d2e32a2 to your computer and use it in GitHub Desktop.
ShadCN UI Data Table with pagination (server, client, infinite), row selection, column visibility, column filtering (server, client), column sorting (server, client).
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useControlledState } from '@/hooks/use-controlled-state';
import { cn } from '@/lib/utils';
import { InfiniteScroll } from '@inertiajs/react';
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
Row,
RowSelectionState,
SortingState,
TableMeta,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { useRef, useState, type MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
className?: string;
meta?: TableMeta<TData>;
sortingOptions?: {
mode: 'server' | 'client';
state?: SortingState;
initialState?: SortingState;
onChange?: (sorting: SortingState) => void;
};
filteringOptions?: {
mode: 'server' | 'client';
state?: ColumnFiltersState;
initialState?: ColumnFiltersState;
onChange?: (filters: ColumnFiltersState) => void;
};
visibilityOptions?: {
state?: VisibilityState;
initialState?: VisibilityState;
onChange?: (visibility: VisibilityState) => void;
};
rowSelectionOptions?: {
state?: RowSelectionState;
initialState?: RowSelectionState;
enableMultiRowSelection?: boolean | ((row: Row<TData>) => boolean);
enableSubRowSelection?: boolean | ((row: Row<TData>) => boolean);
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
onChange?: (selection: RowSelectionState) => void;
onClick?: (row: Row<TData>, event: MouseEvent<HTMLTableRowElement>) => Promise<void> | void;
onError?: (error: unknown, event: MouseEvent<HTMLTableRowElement>) => void;
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
};
paginationOptions?:
| {
mode: 'infinite';
data: string;
as?: string;
preserveUrl?: boolean;
reverse?: boolean;
autoScroll?: boolean;
onlyNext?: boolean;
onlyPrevious?: boolean;
}
| {
mode: 'server';
pageCount?: number;
rowCount?: number;
state?: PaginationState;
initialState?: PaginationState;
onChange?: (pagination: PaginationState) => void;
}
| {
mode: 'client';
state?: PaginationState;
initialState?: PaginationState;
onChange?: (pagination: PaginationState) => void;
};
}
export function DataTable<TData, TValue>({
columns,
data,
meta,
className,
sortingOptions,
filteringOptions,
visibilityOptions,
rowSelectionOptions,
paginationOptions,
}: DataTableProps<TData, TValue>) {
const { t } = useTranslation(['pagination']);
const tableBodyRef = useRef<HTMLTableSectionElement | null>(null);
const tableHeaderRef = useRef<HTMLTableSectionElement | null>(null);
const tableFooterRef = useRef<HTMLTableSectionElement | null>(null);
const [loadingId, setLoadingId] = useState<string | number | null>(null);
const [sorting, onSortingChange] = useControlledState(
sortingOptions?.state,
sortingOptions?.initialState ?? [],
sortingOptions?.onChange,
);
const [columnFilters, onColumnFiltersChange] = useControlledState(
filteringOptions?.state,
filteringOptions?.initialState ?? [],
filteringOptions?.onChange,
);
const [columnVisibility, onColumnVisibilityChange] = useControlledState(
visibilityOptions?.state,
visibilityOptions?.initialState ?? {},
visibilityOptions?.onChange,
);
const [rowSelection, onRowSelectionChange] = useControlledState(
rowSelectionOptions?.state,
rowSelectionOptions?.initialState ?? {},
rowSelectionOptions?.onChange,
);
const [pagination, onPaginationChange] = useControlledState(
paginationOptions?.mode !== 'infinite' ? paginationOptions?.state : undefined,
paginationOptions?.mode !== 'infinite' && paginationOptions?.initialState !== undefined
? paginationOptions.initialState
: { pageSize: 10, pageIndex: 0 },
paginationOptions?.mode !== 'infinite' ? paginationOptions?.onChange : undefined,
);
const table = useReactTable({
data,
meta,
columns,
onSortingChange,
onColumnFiltersChange,
onColumnVisibilityChange,
onRowSelectionChange,
onPaginationChange,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
manualPagination: paginationOptions?.mode !== 'client',
manualSorting: sortingOptions?.mode === 'server',
manualFiltering: filteringOptions?.mode === 'server',
enableMultiRowSelection: rowSelectionOptions?.enableMultiRowSelection,
enableSubRowSelection: rowSelectionOptions?.enableSubRowSelection,
enableRowSelection: rowSelectionOptions?.enableRowSelection,
getRowId: rowSelectionOptions?.getRowId,
pageCount: paginationOptions?.mode === 'server' ? paginationOptions?.pageCount : undefined,
rowCount: paginationOptions?.mode === 'server' ? paginationOptions?.rowCount : undefined,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
const children = (
<Table className={cn('border-separate border-spacing-y-2 sm:border-spacing-y-2.5', className)}>
<TableHeader ref={tableHeaderRef}>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className={cn('bg-transparent hover:bg-transparent', meta?.headerRowClassName)}
>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
colSpan={header.colSpan}
rowSpan={header.rowSpan}
className={cn(
'border-b px-2.5 py-1.5 sm:py-2.5',
meta?.headerClassName,
header.column.columnDef.meta?.headerClassName,
)}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody ref={tableBodyRef}>
{table.getRowModel().rows?.length > 0 ? (
table.getRowModel().rows?.map((row) => (
<TableRow
key={row.id}
className={cn(
'border-none bg-muted',
rowSelectionOptions?.onClick &&
'cursor-pointer data-[loading="true"]:pointer-events-none data-[loading="true"]:opacity-50',
meta?.rowClassName,
)}
data-state={row.getIsSelected() ? 'selected' : 'unselected'}
data-loading={loadingId === row.id}
onClick={async (event) => {
try {
setLoadingId(row.id);
await rowSelectionOptions?.onClick?.(row, event);
} catch (error) {
rowSelectionOptions?.onError?.(error, event);
} finally {
setLoadingId(null);
}
}}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cn(
'px-2 py-2.5 sm:px-2.5 sm:py-5',
'border border-muted',
'in-[tr[data-state="selected"]]:border-primary',
'not-first:not-last:border-x-0',
'first:rounded-tl-lg first:rounded-bl-lg first:border-e-0',
'last:rounded-tr-lg last:rounded-br-lg last:border-s-0',
meta?.cellClassName,
cell.column.columnDef.meta?.cellClassName,
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow data-state="empty" className="border-none bg-muted">
<TableCell colSpan={columns.length} className="h-24 text-center">
{meta?.empty ?? t('noResults')}
</TableCell>
</TableRow>
)}
</TableBody>
<TableFooter ref={tableFooterRef} />
</Table>
);
if (paginationOptions?.mode === 'infinite') {
return (
<InfiniteScroll
itemsElement={tableBodyRef}
startElement={tableHeaderRef}
endElement={tableFooterRef}
data={paginationOptions.data}
as={paginationOptions.as}
preserveUrl={paginationOptions.preserveUrl}
onlyNext={paginationOptions.onlyNext}
onlyPrevious={paginationOptions.onlyPrevious}
reverse={paginationOptions.reverse}
autoScroll={paginationOptions.autoScroll}
>
{children}
</InfiniteScroll>
);
}
return children;
}
import { useCallback, useState } from 'react';
export function useControlledState<T>(
controlledValue: T | undefined,
initialValue: T,
onChange?: (value: T) => void,
): [T, (value: T | ((prev: T) => T)) => void] {
const [internalState, setInternalState] = useState(initialValue);
const isControlled = controlledValue !== undefined;
const state = isControlled ? controlledValue : internalState;
const setState = useCallback(
(value: T | ((prev: T) => T)) => {
const next = typeof value === 'function' ? (value as (prev: T) => T)(state) : value;
if (!isControlled) {
setInternalState(next);
}
onChange?.(next);
},
[isControlled, onChange, state, setInternalState],
);
return [state, setState];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment