DataTable Component Documentation¶
A comprehensive guide to understanding and implementing the DataTable component in the Delta project.
Table of Contents¶
- Overview
- Architecture
- Core Concepts
- Component Structure
- Implementation Guide
- Features
- Advanced Usage
- Performance Considerations
- Testing
- Common Patterns
- API Reference
- Troubleshooting
Overview¶
The DataTable component is a sophisticated, feature-rich table implementation built on top of TanStack Table v8. It provides a modular, composable architecture that supports advanced features like virtualization, drag-and-drop, real-time updates, and extensive customization options.
Key Features¶
- Virtualization: Efficiently render large datasets (10,000+ rows)
- Drag & Drop: Reorder rows and columns
- Real-time Updates: Supabase integration for live data
- Advanced Filtering: Column-specific and global filters
- Sorting: Multi-column sorting with clear sort functionality
- Row Grouping: Group rows by column values with aggregation support
- Export: CSV, Excel, and PDF export capabilities
- Row Selection: Single and multi-row selection
- Column Management: Resize, pin, hide/show columns
- Expandable Rows: Hierarchical data and grouped row expansion
- Responsive: Mobile-first design
- Accessible: Full keyboard navigation and ARIA support
Architecture¶
Design Philosophy¶
The DataTable follows these architectural principles:
- Context-Based State Management: All state is managed through a central context
- Composition Over Inheritance: Features are composed, not inherited
- Separation of Concerns: Logic, presentation, and state are clearly separated
- Hook-Based Logic: Business logic is encapsulated in custom hooks
- Feature Flags: Features can be enabled/disabled through configuration
Component Flow¶
DataTable (Entry Point)
└── DataTableProvider (State Management)
└── DataTableLayout (Visual Structure)
├── DataTableToolbar (Optional)
├── Table
│ ├── DataTableHeader
│ ├── DataTableBody
│ └── DataTableFooter
└── DataTablePagination (Optional)
Core Concepts¶
1. Context Pattern¶
The DataTable uses a context-provider pattern to share state across all child components:
// Context provides centralized state
const context = useDataTableContext();
// Access table instance, state, and methods
const { table, isLoading, error, features } = context;
2. Hook Architecture¶
Logic is organized into specialized hooks:
useDataTableCore: Manages the TanStack Table instanceuseDataTableFeatures: Parses and normalizes feature configurationuseDataTableDragDrop: Handles drag-and-drop functionalityuseDataTableVirtualization: Manages virtualization stateuseEnhancedColumns: Enhances columns with special featuresuseRowGrouping: Manages row grouping state and operations
3. Feature Modules¶
Features are implemented as independent modules that can be composed:
features={{
search: true,
filters: true,
export: { formats: ['csv', 'excel'] },
pagination: { pageSize: 20 },
rowSelection: true,
columnVisibility: true,
}}
Component Structure¶
Directory Organization¶
DataTable/
├── DataTable.tsx # Main entry point
├── DataTableLayout.tsx # Layout component
├── context/
│ ├── DataTableContext.tsx # Context definition
│ ├── DataTableProvider.tsx # Provider implementation
│ └── hooks/ # Context-specific hooks
│ ├── useDataTableCore.ts
│ ├── useDataTableFeatures.ts
│ ├── useDataTableDragDrop.ts
│ └── useDataTableVirtualization.ts
├── components/ # Core components
│ ├── DataTableHeader/
│ ├── DataTableBody/
│ ├── DataTableCell/
│ └── DataTableFooter/
├── features/ # Optional features
│ ├── DataTableToolbar/
│ ├── DataTablePagination/
│ ├── DataTableRowSelection/
│ └── DataTableBulkActions/
├── hooks/ # Utility hooks
│ ├── useEnhancedColumns.ts
│ ├── usePinnedStyles.ts
│ └── useRowGrouping.ts
└── utils/ # Helper functions
├── columnFactories.tsx
├── dataTableHelpers.ts
└── export/
Component Responsibilities¶
Core Components¶
- DataTable: Entry point, wraps provider
- DataTableProvider: Manages all state and logic
- DataTableLayout: Handles layout and feature composition
- DataTableHeader: Renders column headers with sorting/filtering
- DataTableBody: Renders table rows with virtualization support
- DataTableCell: Individual cell rendering with custom renderers
Feature Components¶
- DataTableToolbar: Search, filters, column visibility, export
- DataTablePagination: Page navigation controls
- DataTableRowSelection: Row selection UI
- DataTableBulkActions: Actions for selected rows
- DataTableColumnMenu: Per-column actions menu with sort, filter, pin, group, and aggregation options
- DataTableClearFilters: Clear filters and grouping button
- DataTableColumnMenuGroup: Column grouping submenu
- DataTableColumnMenuAggregation: Column aggregation submenu
Implementation Guide¶
Basic Usage¶
import { DataTable } from '@/components/ui/DataTable';
import { columns } from './columns';
function MyComponent() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<DataTable
data={data ?? []}
columns={columns}
isLoading={isLoading}
error={error}
/>
);
}
Column Definition¶
import { createColumnHelper } from '@tanstack/react-table';
const columnHelper = createColumnHelper<User>();
// Basic columns - most common usage
export const columns = [
// Simple text column - just accessor and header
columnHelper.accessor('name', {
header: 'Name',
}),
// Email with custom filter
columnHelper.accessor('email', {
header: 'Email',
filterFn: 'includesString', // Override default filter
}),
// Number column with data type
columnHelper.accessor('age', {
header: 'Age',
meta: {
dataType: 'number', // Enables number-specific filter operators
},
}),
// Currency column
columnHelper.accessor('salary', {
header: 'Salary',
meta: {
dataType: 'currency',
},
}),
// Date column
columnHelper.accessor('createdAt', {
header: 'Created Date',
meta: {
dataType: 'date',
},
}),
// Boolean column
columnHelper.accessor('isActive', {
header: 'Active',
meta: {
dataType: 'boolean',
},
}),
// Select/enum column
columnHelper.accessor('status', {
header: 'Status',
meta: {
dataType: 'select',
enumOptions: ['pending', 'active', 'inactive'],
},
}),
];
// Advanced columns with custom rendering
export const advancedColumns = [
// Custom header with sorting
columnHelper.accessor('name', {
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
}),
// Custom cell rendering
columnHelper.accessor('priority', {
header: 'Priority',
cell: ({ row }) => {
const priority = row.getValue('priority');
return (
<Badge variant={priority === 'high' ? 'destructive' : 'default'}>
{priority}
</Badge>
);
},
}),
// Column with custom width
columnHelper.accessor('description', {
header: 'Description',
size: 300, // Set column width
minSize: 200,
maxSize: 500,
}),
// Non-sortable column
columnHelper.accessor('notes', {
header: 'Notes',
enableSorting: false,
}),
// Action column (display type)
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<DataTableRowActions
row={row}
actions={[
{
label: 'Edit',
onClick: () => handleEdit(row.original),
},
{
label: 'Delete',
onClick: () => handleDelete(row.original),
variant: 'destructive',
},
]}
/>
),
}),
];
Note: For more comprehensive examples and different use cases, check the Storybook stories in
packages/app/src/components/ui/DataTable/stories/. These stories demonstrate various configurations including filtering, sorting, pagination, row selection, virtualization, and more.
Understanding filterFn¶
The filterFn property determines how column filtering works:
- Default Behavior: All columns use the custom
'operator'filter function by default - Custom Operator Filter: Provides type-aware filtering with multiple operators
- Override Options: Can use TanStack's built-in filters ('fuzzy', 'contains', 'equals', etc.)
Filter Operators by Data Type¶
The default 'operator' filter adapts based on column meta.dataType:
- Text (default): contains, doesn't contain, equals, starts with, ends with, is empty
- Number/Currency: equals, greater than, less than, between, is empty
- Date: equals, after, before, between, is empty
- Select: is any of, is none of, is empty
- Boolean: is true, is false
// Example with data type specification
columnHelper.accessor('price', {
header: 'Price',
meta: {
dataType: 'currency', // Enables currency-specific operators
},
// Automatically uses 'operator' filterFn with currency operators
});
Select columns: single vs multi-value filtering¶
For SELECT columns you can control which operators appear in the filter menu by marking whether the field can hold multiple values.
- Single-value fields (e.g., status, source)
- meta: isMultiValue: false (default)
- Operators shown: isAnyOf, isNoneOf, isEmpty, isNotEmpty
-
Server-side transform:
- isAnyOf → in.(v1,v2)
- isNoneOf → not.in.(v1,v2)
- isAllOf → treated like isAnyOf for single-value fields
-
Multi-value fields (e.g., tags arrays)
- meta: isMultiValue: true
- Operators shown: isAnyOf, isAllOf, isNoneOf, isEmpty, isNotEmpty
- Server-side handling may use array-specific operators (e.g., overlaps, contains) where applicable. See services that map tag filters to tag_names array conditions.
Implementation details:
- Set meta.isMultiValue on the column definition:
- Example: tags column meta: { dataType: 'select', isMultiValue: true, filterOptions: [...] }
- The DataTableColumnMenuFilter reads meta.isMultiValue and passes it to the operator resolver so the correct operator list is shown.
- For large datasets with server-side filtering, prefer meta.filterOptions to avoid computing faceted values on the client.
Enabling Features¶
<DataTable
data={data}
columns={columns}
features={{
// Search
search: {
placeholder: 'Search users...',
columns: ['name', 'email'],
},
// Filtering
filters: {
showClearButton: true,
},
// Pagination
pagination: {
pageSize: 20,
pageSizeOptions: [10, 20, 50, 100],
},
// Row Selection
rowSelection: {
type: 'checkbox',
showInHeader: true,
},
// Export
export: {
formats: ['csv', 'excel', 'pdf'],
filename: 'users',
},
// Column Management
columnVisibility: true,
columnOrdering: true,
columnPinning: true,
columnResizing: true,
// Sorting
sorting: {
multi: true,
},
// Row Grouping
rowGrouping: true,
// Virtualization (must be explicitly enabled)
virtualization: {
enabled: true,
overscan: 5,
},
}}
/>
Features¶
1. Initial Filtering (Default Filters)¶
Set default filters that are applied when the table loads. This is useful for hiding archived, cancelled, or terminated items by default while allowing users to view them when needed.
<DataTable
data={transactions}
columns={columns}
features={{
filtering: true,
}}
options={{
initialColumnFilters: [
{
id: 'status',
value: {
operator: 'isNoneOf',
value: ['cancelled', 'archived'],
},
},
],
}}
/>
Key Behaviors:
- On Load: Table displays data filtered by the initial filters
- "Clear Filters" Button: Visible on initial load (indicating filters are active)
- "Restore Defaults" Button: NOT visible on initial load (table is already in default state)
- After User Modifications: When users add or modify filters, the "Restore Defaults" button appears
- Restore Action: Clicking "Restore Defaults" resets filters to
initialColumnFilters(not completely cleared)
Use Cases:
- Hide archived/cancelled transactions by default
- Show only active employees (filter out terminated)
- Display current orders (exclude completed/cancelled)
- Filter out deleted or inactive records
Note: For server-side filtering, you must also initialize your local state with the same filter values so they're passed to the backend query:
// Initialize both local state and DataTable options with the same filter
const [columnFilters, setColumnFilters] = useState<ColumnFilter[]>([
{
id: 'status',
operator: 'isNoneOf',
value: ['cancelled'],
},
]);
<DataTable
options={{
manualFiltering: true,
onFilter: setColumnFilters,
// filterDebounce: 300, // Default: 300ms. Set to 0 to disable debouncing.
initialColumnFilters: [
{
id: 'status',
value: {
operator: 'isNoneOf',
value: ['cancelled'],
},
},
],
}}
/>
Debouncing: The onFilter callback is automatically debounced (default: 300ms) to prevent burst queries when users rapidly change filters. This eliminates the need for parent components to implement their own debouncing. You can customize the delay with the filterDebounce option or disable it by setting filterDebounce: 0.
See the WithInitialFilters story in DataTable.columnMenu.stories.tsx (in packages/app/src/components/ui/DataTable/stories/) for a working example.
2. Virtualization¶
Virtualization must be explicitly enabled (recommended for tables with 100+ rows):
// Enable virtualization
features={{
virtualization: true, // Simple enable
// or with options:
virtualization: {
rows: true,
columns: false,
overscan: 10, // Number of rows to render outside viewport
}
}}
// Note: The DataTable will warn if virtualization is enabled
// for datasets under 100 rows, as it may not be beneficial
3. Real-time Updates¶
Integrate with Supabase for live updates:
const { data, isLoading, error } = useRealtime({
table: 'users',
select: '*',
orderBy: { column: 'created_at', ascending: false },
});
<DataTable
data={data}
columns={columns}
features={{
realtime: true, // Enable real-time indicators
}}
/>
4. Row Actions¶
Add row-specific actions:
import { DataTableRowActions } from '@/components/ui/DataTable';
const columns = [
// ... other columns
{
id: 'actions',
cell: ({ row }) => (
<DataTableRowActions
row={row}
actions={[
{
label: 'Edit',
onClick: () => handleEdit(row.original),
},
{
label: 'Delete',
onClick: () => handleDelete(row.original),
variant: 'destructive',
},
]}
/>
),
},
];
4.1. Row Click¶
Enable clickable rows for navigation or other actions:
<DataTable
data={data}
columns={columns}
onRowClick={(row) => {
router.push(`/details/${row.id}`);
}}
/>
Key Features:
- Smart Click Detection: Automatically prevents row clicks when:
- Clicking on interactive elements (buttons, links, inputs)
- Clicking on elements with
data-prevent-row-clickattribute - Clicking on drag handles (
data-drag-handleattribute) - Selecting/highlighting text (mouse movement > 5px)
- Row Actions Compatible: Works alongside row action buttons - they trigger separately
- Cursor Indication: Rows show pointer cursor when clickable
- Drag & Drop Integration: When both
onRowClickand row drag-and-drop are enabled: - Automatically enforces handle-mode dragging (must grab handle icon to drag)
- Shows informational warning banner explaining the auto-enforcement
- Prevents accidental navigation during drag operations
Example with Row Actions:
<DataTable
data={products}
columns={columns}
onRowClick={(product) => router.push(`/products/${product.id}`)}
options={{
rowActions: {
actions: [
{
label: 'Edit',
icon: EditIcon,
onClick: (product) => handleEdit(product), // Separate from row click
},
],
displayMode: 'inline',
},
}}
/>
Example with Drag & Drop:
<DataTable
data={tasks}
columns={columns}
onRowClick={(task) => viewTaskDetails(task)}
onReorder={(newData) => setTasks(newData)}
features={{
dragDrop: {
row: true, // Auto-enforces handle mode when onRowClick is present
},
}}
/>
5. Bulk Actions¶
Enable actions on selected rows. Row selection is automatically enabled when bulk actions are provided:
<DataTable
data={data}
columns={columns}
options={{
bulkActions: [
{
label: 'Delete Selected',
icon: TrashIcon,
action: (selectedRows) => handleBulkDelete(selectedRows),
variant: 'destructive',
confirmMessage: 'Are you sure you want to delete the selected items?',
},
{
label: 'Export Selected',
icon: DownloadIcon,
action: (selectedRows) => handleExport(selectedRows),
},
],
}}
/>
Key Features:
- Row selection is automatically enabled when
bulkActionsare provided - Single action displays as a button, multiple actions show as a dropdown
- Supports confirmation dialogs via
confirmMessage - Actions only appear when rows are selected
- Destructive actions are visually separated in the dropdown
5. Custom Cell Renderers¶
Provide custom rendering for specific data types:
<DataTable
data={data}
columns={columns}
cellRenderers={{
date: (value) => format(new Date(value), 'PPP'),
currency: (value) => formatCurrency(value),
boolean: (value) => value ? '✓' : '✗',
}}
/>
6. Row Grouping¶
Group rows by one or more columns with automatic aggregation:
<DataTable
data={data}
columns={columns}
features={{
rowGrouping: true,
}}
/>
// Column definition with aggregation
const columns = [
columnHelper.accessor('department', {
header: 'Department',
// enableGrouping: true is the default - no need to specify
}),
columnHelper.accessor('salary', {
header: 'Salary',
meta: {
dataType: 'currency',
},
aggregationFn: 'sum', // Automatically sum when grouped
aggregatedCell: ({ getValue }) => (
<span className="font-semibold">
Total: {formatCurrency(getValue())}
</span>
),
}),
columnHelper.accessor('count', {
header: 'Count',
meta: {
dataType: 'number',
},
aggregationFn: 'count',
aggregatedCell: ({ getValue }) => (
<span className="text-muted-foreground">
{getValue()} items
</span>
),
}),
];
Grouping Features:¶
- Multi-column grouping: Group by multiple columns simultaneously
- Drag-and-drop grouping: Drag column headers to group by area
- Automatic aggregation: Sum, average, count, etc. based on data type
- Custom aggregation functions: Define custom aggregation logic
- Group expansion: Expand/collapse grouped rows
- Column pinning: Grouped columns are automatically pinned to the left when column pinning is enabled
Available Aggregation Functions:¶
- Number/Currency: sum, average, median, min, max, count, unique count
- Text: count, unique count
- Date: min, max, count, unique count
- Boolean: count true, count false, percentage true
7. Expandable Rows¶
Support hierarchical data and grouped row expansion:
// Hierarchical data with sub-rows
<DataTable
data={data}
columns={columns}
features={{
rowExpanding: {
enabled: true,
renderSubComponent: ({ row }) => (
<SubRowDetails data={row.original} />
),
},
}}
getSubRows={(row) => row.children}
/>
// Expandable rows work automatically with grouping
<DataTable
data={data}
columns={columns}
features={{
rowGrouping: true,
rowExpanding: true, // Grouped rows are expandable by default
}}
/>
Expansion Features:¶
- Hierarchical data: Support for nested data structures
- Lazy loading: Load sub-rows on demand
- Custom sub-components: Render any content in expanded rows
- Keyboard navigation: Arrow keys to expand/collapse
- Auto-expansion: Optionally expand rows by default
- Works with grouping: Grouped rows can be expanded/collapsed
8. Inline Editing¶
The DataTable supports comprehensive inline editing with two modes and two save strategies:
Edit Modes¶
- Cell Mode (default): Edit individual cells one at a time
- Row Mode: Edit entire rows at once
Save Strategies¶
- Immediate Mode: Changes are saved immediately when a cell loses focus
- Batch Mode: Changes are collected and saved together
Basic Inline Editing¶
// Basic cell editing with immediate save
<DataTable
data={data}
columns={columns}
features={{
inlineEdit: {
enabled: true,
onCellEdit: async (rowIndex, columnId, value) => {
// Save to backend
await updateData(rowIndex, columnId, value);
// Update local state
setData(prev => {
const newData = [...prev];
newData[rowIndex][columnId] = value;
return newData;
});
},
},
}}
/>
Column Configuration for Editing¶
Define which columns are editable and how they should behave:
const columns = [
{
id: 'name',
accessorKey: 'name',
header: 'Name',
meta: {
editable: true,
required: true, // Hides clear button in inputs/selects
editType: 'text',
validateEdit: (value) => {
if (!value || value.trim().length === 0) {
return 'Name is required';
}
return true;
},
},
},
{
id: 'price',
accessorKey: 'price',
header: 'Price',
meta: {
editable: true,
editType: 'number',
dataType: 'currency',
currency: 'USD',
validateEdit: (value) => {
const num = Number(value);
if (isNaN(num) || num < 0) {
return 'Price must be non-negative';
}
return true;
},
},
},
{
id: 'status',
accessorKey: 'status',
header: 'Status',
meta: {
editable: true,
editType: 'select',
editOptions: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
],
required: true,
},
},
{
id: 'deliveryDate',
accessorKey: 'deliveryDate',
header: 'Delivery Date',
meta: {
editable: true,
editType: 'date',
dataType: 'date',
},
},
{
id: 'isActive',
accessorKey: 'isActive',
header: 'Active',
meta: {
editable: true,
editType: 'checkbox',
dataType: 'boolean',
},
},
{
id: 'isEnabled',
accessorKey: 'isEnabled',
header: 'Enabled',
meta: {
editable: true,
editType: 'switch', // Use switch instead of checkbox
dataType: 'boolean',
},
},
];
Supported Edit Types¶
- text: Text input field
- number: Number input field
- select: Dropdown selection
- date: Date picker
- checkbox: Boolean checkbox
- switch: Boolean switch toggle
Batch Mode Editing¶
Collect changes and save them together:
function BatchEditExample() {
const [data, setData] = useState(initialData);
const tableRef = useRef();
const handleSave = async () => {
const inlineEdit = tableRef.current?.inlineEdit;
if (!inlineEdit) return;
const changes = inlineEdit.getEditedData();
// Save all changes to backend
await saveChanges({
added: changes.added,
modified: changes.modified,
deleted: changes.deleted,
});
// Clear pending changes after successful save
inlineEdit.clearPendingChanges();
};
const handleCancel = () => {
const inlineEdit = tableRef.current?.inlineEdit;
if (!inlineEdit) return;
// Discard all pending changes
inlineEdit.discardChanges();
};
return (
<>
<div className="flex gap-2 mb-4">
<Button onClick={handleSave}>Save Changes</Button>
<Button variant="outline" onClick={handleCancel}>Cancel</Button>
</div>
<DataTable
ref={tableRef}
data={data}
columns={columns}
features={{
inlineEdit: {
enabled: true,
saveMode: 'batch',
mode: 'cell',
},
}}
/>
</>
);
}
Row Mode Editing¶
Edit entire rows at once:
<DataTable
data={data}
columns={columns}
features={{
inlineEdit: {
enabled: true,
mode: 'row', // Enable row mode
saveMode: 'batch',
onRowEdit: async (rowIndex, rowData) => {
// Save entire row
await updateRow(rowIndex, rowData);
},
},
}}
/>
Always Editable Mode¶
Make cells immediately editable without requiring a click:
<DataTable
data={data}
columns={columns}
features={{
inlineEdit: {
enabled: true,
alwaysEditable: true, // No click required to edit
saveMode: 'batch',
},
}}
/>
Row Addition and Deletion¶
Add support for adding and deleting rows:
<DataTable
data={data}
columns={columns}
features={{
inlineEdit: {
enabled: true,
saveMode: 'batch',
rowAddition: {
enabled: true,
position: 'bottom',
autoEdit: true, // Automatically start editing new rows
getDefaultRow: () => ({
name: '',
price: 0,
status: 'active',
deliveryDate: new Date(),
}),
},
rowDeletion: {
enabled: true,
confirmMessage: (row) => `Delete ${row.name}?`,
onDelete: async (rowIndex, row) => {
if (saveMode === 'immediate') {
await deleteRow(row.id);
}
// In batch mode, deletion is tracked internally
},
},
},
}}
/>
Validation¶
Columns support custom validation functions:
{
meta: {
editable: true,
validateEdit: (value) => {
// Return true if valid
// Return error message string if invalid
if (!value) return 'This field is required';
if (value.length < 3) return 'Minimum 3 characters';
return true;
},
},
}
Working with Calculated Fields¶
When cells depend on other cells (e.g., tax calculations), handle updates in the onCellEdit callback:
onCellEdit: async (rowIndex, columnId, value) => {
const newData = [...data];
newData[rowIndex][columnId] = value;
// Recalculate dependent fields
if (columnId === 'quantity' || columnId === 'unit_price') {
const row = newData[rowIndex];
row.subtotal = row.quantity * row.unit_price;
row.tax = row.subtotal * row.tax_rate;
row.total = row.subtotal + row.tax;
}
if (columnId === 'tax_rate_id') {
const taxRate = taxRates.find((t) => t.id === value);
const row = newData[rowIndex];
row.tax = (row.subtotal * (taxRate?.percentage || 0)) / 100;
row.total = row.subtotal + row.tax;
}
setData(newData);
};
Advanced Features¶
- Click Outside Handling: Clicking outside the table cancels edit mode (unless alwaysEditable is true)
- Keyboard Navigation: Tab/Enter to move between cells, Escape to cancel editing
- Optimistic Updates: Show changes immediately while saving in background
- Error Handling: Display validation errors inline
- Undo/Redo: Track changes for undo functionality (in batch mode)
Batch Mode with External State Management¶
When using batch mode with external state management, changes are tracked internally by the DataTable but the actual data state is managed externally. This is the recommended approach for complex forms and transactions.
Key Concepts¶
- External State: The data array is managed outside the DataTable
- Change Tracking: DataTable tracks which cells have been edited
- Pending Changes: All edits are collected until explicitly saved
- Visual Indicators: Modified cells show a visual indicator (amber background)
Implementation Pattern¶
function TransactionEditForm() {
const [transaction, setTransaction] = useState(initialTransaction);
const [editedLineItems, setEditedLineItems] = useState(transaction.line_items || []);
const tableRef = useRef<DataTableRef<LineItem>>(null);
// Handle cell edits with external state management
const handleCellEdit = (rowIndex: number, columnId: string, value: unknown) => {
setEditedLineItems(prevItems => {
const newItems = [...prevItems];
const currentItem = newItems[rowIndex];
// Track changes for existing items
if (!currentItem._isNew) {
currentItem._changes = currentItem._changes || {};
currentItem._changes[columnId] = true;
}
// Update the field value
currentItem[columnId] = value;
// Recalculate computed fields (e.g., totals)
if (columnId === 'quantity' || columnId === 'unit_price') {
currentItem.subtotal = currentItem.quantity * currentItem.unit_price;
currentItem.total = currentItem.subtotal + currentItem.tax_total;
}
return newItems;
});
};
// Handle row addition
const handleRowAdd = (newRow: Partial<LineItem>) => {
const newItem = {
...newRow,
id: `temp_${Date.now()}`,
_isNew: true,
};
setEditedLineItems(prev => [...prev, newItem]);
};
// Handle row deletion
const handleRowDelete = (rowIndex: number, row: LineItem) => {
setEditedLineItems(prev => {
const newItems = [...prev];
newItems[rowIndex] = { ...newItems[rowIndex], _isDeleted: true };
return newItems;
});
};
// Save all changes
const handleSave = async () => {
const added = editedLineItems.filter(item => item._isNew && !item._isDeleted);
const modified = editedLineItems.filter(item =>
!item._isNew && item._changes && Object.keys(item._changes).length > 0
);
const deleted = editedLineItems.filter(item => item._isDeleted);
// Save to backend
await saveChanges({ added, modified, deleted });
// Clear change tracking
setEditedLineItems(prev => prev.map(item => ({
...item,
_isNew: false,
_isDeleted: false,
_changes: {}
})));
};
return (
<DataTableProvider
ref={tableRef}
columns={columns}
data={editedLineItems}
features={{
inlineEdit: {
enabled: true,
saveMode: SAVE_MODE.BATCH,
alwaysEditable: true,
onCellEdit: handleCellEdit,
rowAddition: {
enabled: true,
onRowAdd: handleRowAdd,
},
rowDeletion: {
enabled: true,
onDelete: handleRowDelete,
}
}
}}
>
<DataTableLayout />
</DataTableProvider>
);
}
Change Tracking Pattern¶
Use metadata properties to track changes:
interface TrackedItem {
// Original fields
id: string;
name: string;
price: number;
// Tracking metadata
_isNew?: boolean; // Item was added in this session
_isDeleted?: boolean; // Item marked for deletion
_changes?: Record<string, boolean>; // Which fields have been edited
}
DataTableRef - Accessing Table Methods¶
The DataTableRef provides programmatic access to table methods and state from parent components.
Available Methods¶
interface DataTableRef<TData> {
// Table instance access
getTable(): Table<TData>;
// Data access
getData(): TData[];
getFilteredData(): TData[];
getSelectedRows(): TData[];
// Inline editing methods (batch mode)
getEditedData(): {
added: TData[];
modified: TData[];
deleted: TData[];
};
getPendingChanges(): Array<{
rowIndex: number;
columnId: string;
value: unknown;
}>;
clearPendingChanges(): void;
discardChanges(): void;
isDirty(): boolean;
// Edit control
startEdit(rowIndex: number, columnId: string): void;
cancelEdit(): void;
saveEdit(rowIndex: number, columnId: string, value: unknown): void;
// Row operations
addRow(data: Partial<TData>): void;
deleteRow(rowIndex: number): void;
updateRow(rowIndex: number, data: Partial<TData>): void;
// Export methods
exportToCSV(): void;
exportToExcel(): void;
exportToPDF(): void;
// Refresh/reset
refresh(): void;
reset(): void;
}
Usage Example¶
function ParentFormWithTable() {
const tableRef = useRef<DataTableRef<Product>>(null);
const [formData, setFormData] = useState(initialFormData);
const handleSubmit = async () => {
// Access table methods via ref
const editedData = tableRef.current?.getEditedData();
const selectedRows = tableRef.current?.getSelectedRows();
if (tableRef.current?.isDirty()) {
const confirmation = await confirm('Save pending changes?');
if (!confirmation) {
tableRef.current.discardChanges();
return;
}
}
// Combine form data with table data
const payload = {
...formData,
items: editedData?.modified || [],
selectedItems: selectedRows || [],
};
await submitForm(payload);
// Clear table state after successful save
tableRef.current?.clearPendingChanges();
};
const handleExport = () => {
// Export filtered data
tableRef.current?.exportToExcel();
};
const handleReset = () => {
// Reset table to initial state
tableRef.current?.reset();
setFormData(initialFormData);
};
return (
<>
<form>
{/* Form fields */}
</form>
<DataTableProvider
ref={tableRef}
columns={columns}
data={products}
features={{
inlineEdit: {
enabled: true,
saveMode: SAVE_MODE.BATCH,
},
rowSelection: true,
export: {
csv: true,
excel: true,
}
}}
>
<DataTableLayout />
</DataTableProvider>
<div className="flex gap-2">
<Button onClick={handleSubmit}>Submit</Button>
<Button onClick={handleExport}>Export</Button>
<Button onClick={handleReset}>Reset</Button>
</div>
</>
);
}
Best Practices for Ref Usage¶
- Always check for null: The ref might not be available immediately
- Use optional chaining:
tableRef.current?.method() - Avoid direct state manipulation: Use provided methods
- Handle async operations: Many methods return promises
// Good
const isDirty = tableRef.current?.isDirty() ?? false;
// Bad - direct state access
const table = tableRef.current?.getTable();
table.state.rowSelection = {}; // Don't do this!
// Good - use provided methods
tableRef.current?.clearSelection();
9. Drag & Drop¶
Enable row and/or column reordering:
// Basic row drag & drop
<DataTable
data={data}
columns={columns}
features={{
dragDrop: {
row: true,
},
}}
onReorder={(newData) => setData(newData)}
/>
// Row drag with handle mode
<DataTable
data={data}
columns={columns}
features={{
dragDrop: {
row: {
mode: 'handle',
showHandle: true,
},
},
}}
onReorder={(newData) => setData(newData)}
/>
// Both row and column drag & drop
<DataTable
data={data}
columns={columns}
features={{
dragDrop: {
row: {
mode: 'handle',
showHandle: true,
},
column: true,
},
}}
onReorder={(newData) => setData(newData)}
onColumnOrderChange={(columnOrder) => console.log(columnOrder)}
/>
Note: The drag handle for rows appears integrated in the first data column (or first left-pinned data column if any).
Advanced Usage¶
Custom Styling¶
<DataTable
data={data}
columns={columns}
className="custom-table"
classNames={{
wrapper: 'custom-wrapper',
table: 'custom-table-element',
header: 'custom-header',
body: 'custom-body',
row: 'custom-row',
cell: 'custom-cell',
}}
/>
Controlled State¶
function ControlledTable() {
const [sorting, setSorting] = useState<SortingState>([]);
const [filters, setFilters] = useState<ColumnFiltersState>([]);
return (
<DataTable
data={data}
columns={columns}
state={{
sorting,
columnFilters: filters,
}}
onSortingChange={setSorting}
onColumnFiltersChange={setFilters}
/>
);
}
Custom Empty State¶
<DataTable
data={data}
columns={columns}
emptyState={{
icon: <SearchIcon />,
title: 'No results found',
description: 'Try adjusting your search or filters',
action: {
label: 'Clear filters',
onClick: () => table.resetColumnFilters(),
},
}}
/>
Integration with Forms¶
function FormWithTable() {
const [selectedRows, setSelectedRows] = useState([]);
return (
<form onSubmit={handleSubmit}>
<DataTable
data={data}
columns={columns}
features={{
rowSelection: {
type: 'checkbox',
showInHeader: true,
},
}}
onRowSelectionChange={(selection) => {
setSelectedRows(Object.keys(selection));
}}
/>
<Button type="submit">
Process {selectedRows.length} items
</Button>
</form>
);
}
Performance Considerations¶
1. Virtualization¶
- Must be explicitly enabled via
features.virtualization - Recommended for datasets with 100+ rows
- Reduces DOM nodes from thousands to ~20-30
- Maintains smooth scrolling performance
- Shows warning if enabled for datasets under 100 rows
2. Memoization¶
- Column definitions should be memoized
- Use
React.memofor custom cell components - Avoid inline functions in column definitions
// Good
const columns = useMemo(() => [
columnHelper.accessor('name', {
header: 'Name',
cell: NameCell, // Pre-defined component
}),
], []);
// Avoid
const columns = [
{
cell: ({ row }) => <div>{row.name}</div>, // Inline function
},
];
3. Data Updates¶
- Use
useRealtimehook for live updates - Implement proper query invalidation
- Consider pagination for large datasets
4. Feature Impact¶
Features have varying performance impacts:
- Low Impact: Sorting, filtering, search
- Medium Impact: Row selection, column resizing
- High Impact: Virtualization (improves performance), drag-and-drop
Testing¶
Unit Testing¶
import { render, screen } from '@testing-library/react';
import { DataTable } from '@/components/ui/DataTable';
describe('DataTable', () => {
it('renders data correctly', () => {
render(
<DataTable
data={mockData}
columns={mockColumns}
data-testid="users-table"
/>
);
expect(screen.getByTestId('users-table')).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
it('shows loading state', () => {
render(
<DataTable
data={[]}
columns={mockColumns}
isLoading={true}
/>
);
expect(screen.getByTestId('data-table-loading')).toBeInTheDocument();
});
});
Integration Testing¶
it('handles row selection', async () => {
const user = userEvent.setup();
const onSelectionChange = jest.fn();
render(
<DataTable
data={mockData}
columns={mockColumns}
features={{ rowSelection: true }}
onRowSelectionChange={onSelectionChange}
/>
);
const checkbox = screen.getByRole('checkbox', { name: /select row/i });
await user.click(checkbox);
expect(onSelectionChange).toHaveBeenCalled();
});
Common Patterns¶
1. Server-Side Operations¶
function ServerSideTable() {
const [{ pageIndex, pageSize }, setPagination] = useState({
pageIndex: 0,
pageSize: 20,
});
const { data, isLoading } = useQuery({
queryKey: ['users', pageIndex, pageSize],
queryFn: () => fetchUsers({ page: pageIndex, limit: pageSize }),
});
return (
<DataTable
data={data?.items ?? []}
columns={columns}
isLoading={isLoading}
features={{
pagination: {
pageSize,
pageCount: data?.pageCount,
manualPagination: true,
},
}}
onPaginationChange={setPagination}
/>
);
}
2. Dynamic Columns¶
function DynamicColumnsTable() {
const [visibleColumns, setVisibleColumns] = useState(['name', 'email']);
const columns = useMemo(() =>
baseColumns.filter(col => visibleColumns.includes(col.id)),
[visibleColumns]
);
return (
<DataTable
data={data}
columns={columns}
features={{
columnVisibility: {
onVisibilityChange: setVisibleColumns,
},
}}
/>
);
}
3. Row Expansion with Lazy Loading¶
function LazyExpandableTable() {
const [expandedData, setExpandedData] = useState({});
return (
<DataTable
data={data}
columns={columns}
features={{
rowExpanding: {
enabled: true,
renderSubComponent: ({ row }) => {
const subData = expandedData[row.id];
if (!subData) {
loadSubData(row.id).then(data => {
setExpandedData(prev => ({ ...prev, [row.id]: data }));
});
return <div>Loading...</div>;
}
return <SubComponent data={subData} />;
},
},
}}
/>
);
}
4. Programmatic Grouping Control¶
function ControlledGroupingTable() {
// Access grouping controls via ref
const tableRef = useRef<DataTableRef>(null);
const handleGroupByDepartment = () => {
// Group by department column
tableRef.current?.setGrouping(['department']);
};
const handleGroupByMultiple = () => {
// Group by multiple columns
tableRef.current?.setGrouping(['department', 'role']);
};
const handleClearGrouping = () => {
// Clear all grouping
tableRef.current?.resetGrouping();
};
return (
<>
<div className="flex gap-2 mb-4">
<Button onClick={handleGroupByDepartment}>
Group by Department
</Button>
<Button onClick={handleGroupByMultiple}>
Group by Dept & Role
</Button>
<Button onClick={handleClearGrouping} variant="outline">
Clear Grouping
</Button>
</div>
<DataTable
ref={tableRef}
data={data}
columns={columns}
features={{
rowGrouping: true,
}}
/>
</>
);
}
5. Footer Aggregation¶
The DataTable supports footer aggregation to display summary statistics:
// Basic footer aggregation
const columns = [
columnHelper.accessor('amount', {
header: 'Amount',
meta: {
dataType: 'currency',
footerAggregation: 'sum', // Built-in aggregation
},
}),
columnHelper.accessor('quantity', {
header: 'Quantity',
meta: {
footerAggregation: 'count',
},
}),
];
// Enable footer with aggregation labels
<DataTable
data={data}
columns={columns}
options={{
showFooter: {
showAggregationLabels: true, // Shows "Total: $1,234.56"
},
locale: 'en-US',
currency: 'USD',
}}
/>
Available Aggregation Functions¶
- sum: Total of all values
- min: Minimum value
- max: Maximum value
- mean: Average value
- median: Middle value
- count: Count of non-null values
- unique: Count of unique values
- extent: Range [min, max]
Custom Footer Content¶
// Custom footer with precedence over aggregation
columnHelper.accessor('total', {
header: 'Total',
footer: 'Grand Total:', // Takes precedence
meta: {
footerAggregation: 'sum', // Ignored when footer is set
},
});
// Dynamic footer function
columnHelper.accessor('status', {
header: 'Status',
footer: (context) => {
const completedCount = context.table
.getFilteredRowModel()
.rows.filter((row) => row.getValue('status') === 'completed').length;
return `Completed: ${completedCount}`;
},
});
6. Custom Aggregation Functions¶
// Define custom aggregation function
const customMedian: AggregationFn<any> = (columnId, leafRows) => {
const values = leafRows
.map(row => row.getValue(columnId))
.filter(val => typeof val === 'number')
.sort((a, b) => a - b);
const mid = Math.floor(values.length / 2);
return values.length % 2 !== 0
? values[mid]
: (values[mid - 1] + values[mid]) / 2;
};
// Use in column definition
const columns = [
columnHelper.accessor('score', {
header: 'Score',
aggregationFn: customMedian,
aggregatedCell: ({ getValue }) => (
<span>Median: {getValue()}</span>
),
}),
];
API Reference¶
DataTable Props¶
interface DataTableProps<TData> {
// Required
columns: ColumnDef<TData>[];
data: TData[];
// Optional
features?: DataTableFeatures;
options?: DataTableOptions<TData>;
// Callbacks
onRowClick?: (row: Row<TData>) => void;
onSelectionChange?: (selection: RowSelectionState) => void;
onExpandedChange?: (expanded: ExpandedState) => void;
onGroupingChange?: (grouping: GroupingState) => void;
onColumnPinningChange?: (pinning: ColumnPinningState) => void;
onColumnVisibilityChange?: (visibility: VisibilityState) => void;
onRowPinningChange?: (pinning: RowPinningState) => void;
onReorder?: (fromIndex: number, toIndex: number) => void;
onColumnOrderChange?: (columnOrder: string[]) => void;
onPaginationChange?: (pageIndex: number, pageSize: number) => void;
onRealtimeUpdate?: (payload: RealtimePayload<TData>) => void;
// UI Props
className?: string;
containerClassName?: string;
testId?: string;
title?: string;
isLoading?: boolean;
error?: Error | null;
emptyMessage?: string;
emptyIcon?: ReactNode;
// Real-time
realtimeTable?: string;
// Custom Renderers
renderSubComponent?: (props: { row: Row<TData> }) => ReactElement;
renderGroupHeader?: (group: GroupingRowModel<TData>) => ReactNode;
}
DataTableFeatures¶
All features can be boolean (enable with defaults) or configuration objects:
interface DataTableFeatures {
// Sorting
sorting?:
| boolean
| {
multi?: boolean; // Allow multi-column sorting
removeSortingOnColumnHide?: boolean;
};
// Filtering
filtering?:
| boolean
| {
showClearButton?: boolean;
debounceMs?: number;
};
// Global Search
globalSearch?:
| boolean
| {
placeholder?: string;
debounceMs?: number;
includeColumns?: string[]; // Specific columns to search
};
// Pagination
pagination?:
| boolean
| {
pageSize?: number;
pageSizeOptions?: number[];
showPageJump?: boolean;
showRowCount?: boolean;
};
// Column Features
columnPinning?:
| boolean
| {
initialPinning?: ColumnPinningState;
};
columnResizing?:
| boolean
| {
mode?: 'onChange' | 'onEnd';
columnResizeDirection?: 'ltr' | 'rtl';
};
columnOrdering?:
| boolean
| {
initialOrder?: string[];
};
columnVisibility?:
| boolean
| {
initialVisibility?: VisibilityState;
showToggleAll?: boolean;
};
// Row Features
rowSelection?:
| boolean
| {
type?: 'checkbox' | 'radio';
showInHeader?: boolean;
enableClickSelection?: boolean;
}
| ((row: Row<TData>) => boolean); // Function for row-specific enable/disable
rowExpanding?:
| boolean
| {
expandOnRowClick?: boolean;
allowMultiple?: boolean;
};
rowPinning?:
| boolean
| {
includeLeafRows?: boolean;
includeParentRows?: boolean;
};
rowGrouping?:
| boolean
| {
defaultExpanded?: boolean;
manualGrouping?: boolean;
};
// Export
export?: boolean | DataTableExportOptions;
// Drag & Drop
dragDrop?: boolean | DataTableDragDropConfig;
// Virtualization
virtualization?: boolean | DataTableVirtualizationConfig;
// Inline Editing
inlineEdit?: boolean | DataTableInlineEditConfig;
}
DataTableInlineEditConfig¶
interface DataTableInlineEditConfig {
enabled?: boolean; // Enable inline editing
mode?: 'cell' | 'row'; // Edit mode - cell-by-cell or entire row
saveMode?: SAVE_MODE; // Save immediately or batch changes
alwaysEditable?: boolean; // Always in edit mode (no click required)
editableColumns?: string[]; // Specific columns that are editable
// Callbacks
onCellEdit?: (
rowIndex: number,
columnId: string,
value: unknown
) => void | Promise<void>;
onRowEdit?: (
rowIndex: number,
data: Record<string, unknown>
) => void | Promise<void>;
// Row operations
rowDeletion?: {
enabled?: boolean;
confirmMessage?: string | ((row: TData) => string);
onDelete?: (rowIndex: number, row: TData) => void | Promise<void>;
};
rowAddition?: {
enabled?: boolean;
position?: 'top' | 'bottom';
autoEdit?: boolean; // Auto-start editing new rows
getDefaultRow?: () => Partial<TData>;
onAdd?: (row: TData) => void | Promise<void>;
};
// Batch mode methods (available when saveMode === 'batch')
getEditedData?: () => {
added: TData[];
modified: Array<{ original: TData; modified: Partial<TData> }>;
deleted: TData[];
};
clearPendingChanges?: () => void;
discardChanges?: () => void;
isDirty?: boolean;
}
DataTableOptions¶
interface DataTableOptions<TData> {
// Display Options
stickyHeader?: boolean; // Default: true
showFooter?:
| boolean
| {
showAggregationLabels?: boolean; // Show "Total:", "Average:" etc.
};
striped?: boolean; // Default: false
maxHeight?: number | string; // Container max height
// Localization
locale?: string; // Default: 'en-IN'
currency?: string; // Default: 'INR'
// Pagination
pageSize?: number; // Default: 10
pageSizeOptions?: number[]; // Default: [10, 20, 30, 40, 50]
pageCount?: number; // For server-side pagination
totalRows?: number; // Total row count for server-side
showPageJump?: boolean;
paginationLoading?: boolean;
// Row Actions
rowActions?: DataTableRowAction<TData>[];
pinSelection?: boolean; // Pin selection column
pinActions?: boolean; // Pin actions column
// Bulk Actions (automatically enables row selection)
bulkActions?: DataTableBulkAction<TData>[];
// Hierarchical Data
getSubRows?: (originalRow: TData, index: number) => TData[] | undefined;
// Initial States
initialRowPinning?: RowPinningState;
initialColumnPinning?: ColumnPinningState;
initialColumnFilters?: ColumnFiltersState;
// Server-side Options
manualPagination?: boolean;
manualSorting?: boolean;
manualFiltering?: boolean;
manualGrouping?: boolean;
filterDebounce?: number; // Debounce delay (ms) for onFilter callback. Default: 300. Set to 0 to disable.
// Aggregation
enableGrouping?: boolean;
columnResizeMode?: 'onChange' | 'onEnd';
columnResizeDirection?: 'ltr' | 'rtl';
// Custom Options
meta?: Record<string, any>; // Custom metadata
}
DataTableExportOptions¶
interface DataTableExportOptions {
csv?: boolean;
excel?: boolean;
pdf?: boolean;
json?: boolean;
// Or as object with options
csv?: {
enabled?: boolean;
filename?: string;
includeHeaders?: boolean;
delimiter?: string;
};
excel?: {
enabled?: boolean;
filename?: string;
sheetName?: string;
includeHeaders?: boolean;
};
pdf?: {
enabled?: boolean;
filename?: string;
title?: string;
orientation?: 'portrait' | 'landscape';
pageSize?: 'A4' | 'Letter' | 'Legal';
};
}
DataTableDragDropConfig¶
interface DataTableDragDropConfig {
rows?:
| boolean
| {
enabled?: boolean;
onReorder?: (fromIndex: number, toIndex: number) => void;
allowBetweenGroups?: boolean;
};
columns?:
| boolean
| {
enabled?: boolean;
onReorder?: (columnOrder: string[]) => void;
};
hierarchical?:
| boolean
| {
enabled?: boolean;
indentPerLevel?: number; // Default: 8px
maxDepth?: number;
};
}
DataTableVirtualizationConfig¶
interface DataTableVirtualizationConfig {
enabled?: boolean; // Can also be just `true` to enable with defaults
rows?:
| boolean
| {
enabled?: boolean;
overscanCount?: number; // Default: 10
estimateSize?: number; // Default: 54
measureElement?: boolean; // Enable dynamic row height
};
columns?:
| boolean
| {
enabled?: boolean;
overscanCount?: number; // Default: 5
estimateSize?: number; // Default: 180
};
// Infinite scroll
infiniteScroll?: {
enabled?: boolean;
loadMore: () => Promise<void>;
hasMore: boolean;
loadingMore?: boolean;
threshold?: number; // Default: 200px
};
}
DataTableBulkAction¶
interface DataTableBulkAction<TData> {
label: string;
action: (selectedRows: TData[]) => void | Promise<void>;
icon?: ComponentType<{ className?: string }>;
variant?:
| 'primary'
| 'destructive'
| 'outline'
| 'secondary'
| 'ghost'
| 'link';
confirmMessage?: string;
requireConfirmation?: boolean;
}
Column Meta Options¶
Additional column configuration via the meta property:
interface ColumnMeta {
// Data Type
dataType?:
| 'text'
| 'number'
| 'currency'
| 'percentage'
| 'date'
| 'dateTime'
| 'boolean'
| 'select'
| 'email'
| 'url'
| 'phone';
// For currency columns
currency?: string; // Override table currency
locale?: string; // Override locale for formatting
// For select columns
enumOptions?: string[] | { value: string; label: string }[];
// Inline Editing
editable?: boolean; // Whether this column is editable
required?: boolean; // Whether field is required (hides clear button)
editType?: 'text' | 'number' | 'select' | 'date' | 'checkbox' | 'switch'; // Edit input type
editOptions?: Array<{ label: string; value: string }>; // Options for select type
validateEdit?: (value: unknown) => boolean | string; // Validation function
// Export control
excludeFromExport?: boolean;
exportValue?: (value: any) => string; // Custom export formatter
// Footer aggregation
footerAggregation?:
| 'sum'
| 'min'
| 'max'
| 'mean'
| 'median'
| 'count'
| 'unique'
| 'extent';
// Pinning
pinned?: 'left' | 'right';
// Custom metadata
[key: string]: any;
}
Usage Examples¶
Basic Usage¶
<DataTable
data={users}
columns={columns}
/>
With Features¶
<DataTable
data={users}
columns={columns}
features={{
sorting: true,
filtering: true,
pagination: {
pageSize: 20,
showPageJump: true,
},
export: {
csv: true,
excel: true,
pdf: {
enabled: true,
orientation: 'landscape',
},
},
rowSelection: {
type: 'checkbox',
showInHeader: true,
},
}}
options={{
stickyHeader: true,
striped: true,
showFooter: {
showAggregationLabels: true,
},
locale: 'en-US',
currency: 'USD',
}}
/>
Server-side Pagination¶
<DataTable
data={currentPageData}
columns={columns}
features={{
pagination: true,
}}
options={{
manualPagination: true,
pageCount: Math.ceil(totalRows / pageSize),
totalRows,
}}
onPaginationChange={(pageIndex, pageSize) => {
// Fetch new page data
}}
/>
With Virtualization¶
<DataTable
data={largeDataset} // 10,000+ rows
columns={columns}
features={{
virtualization: {
rows: {
enabled: true,
overscanCount: 10,
},
columns: {
enabled: true, // For many columns
},
},
}}
options={{
maxHeight: '600px', // Required for virtualization
}}
/>
Troubleshooting¶
Common Issues¶
1. Virtualization Not Working¶
Problem: Table renders all rows instead of virtualizing
Solution:
// Ensure container has fixed height
<div className="h-[600px]">
<DataTable data={data} columns={columns} />
</div>
2. Column Filters Not Working¶
Problem: Column filters don't filter data
Solution:
// Add filterFn to column definition
{
accessorKey: 'status',
filterFn: 'equals', // or 'includesString', 'arrIncludesSome', etc.
}
3. Performance Issues¶
Problem: Table is slow with large datasets
Solutions:
- Enable virtualization
- Implement server-side pagination
- Memoize column definitions
- Use
React.memofor custom cells
4. State Not Updating¶
Problem: Table doesn't reflect state changes
Solution:
// Ensure proper key for re-renders
<DataTable
key={data.length} // Force re-render on data change
data={data}
columns={columns}
/>
5. Grouping Not Working¶
Problem: Columns can't be grouped or aggregation not showing
Solutions:
// Disable grouping on a specific column (enabled by default)
{
accessorKey: 'id',
enableGrouping: false, // Explicitly disable for columns that shouldn't be grouped
}
// Check feature is enabled
features={{
rowGrouping: true, // or { enabled: true }
}}
// For aggregation, specify aggregationFn
{
accessorKey: 'amount',
aggregationFn: 'sum', // Built-in function
aggregatedCell: ({ getValue }) => getValue(), // Custom display
}
6. Grouped Columns Not Auto-Pinning¶
Problem: Grouped columns aren't automatically pinned to left
Solution:
// Ensure column pinning feature is enabled
features={{
rowGrouping: true,
columnPinning: true, // Required for auto-pinning grouped columns
}}
// When both features are enabled, the useRowGrouping hook
// automatically pins grouped columns to the left (after special
// columns like select)
Debugging Tips¶
- Enable Debug Mode:
<DataTable debug={true} ... />
- Check Context Values:
const Component = () => {
const context = useDataTableContext();
console.log('Table State:', context.table.getState());
};
- Monitor Performance:
// Use React DevTools Profiler
// Check render counts and duration
- Validate Props:
// DataTable will warn about invalid feature combinations
// Check console for warnings
Best Practices¶
- Always memoize columns to prevent unnecessary re-renders
- Use proper TypeScript types for data and column definitions
- Enable features progressively - start simple, add features as needed
- Test with realistic data volumes to ensure performance
- Provide meaningful empty states for better UX
- Use semantic HTML and ARIA labels for accessibility
- Implement proper error boundaries for error handling
- Consider mobile experience - use responsive features
- Document custom renderers for team understanding
- Monitor bundle size when adding export features
Conclusion¶
The DataTable component provides a powerful, flexible solution for displaying and interacting with tabular data. By understanding its architecture and following the patterns outlined in this guide, you can effectively implement complex table requirements while maintaining good performance and user experience.
For specific implementation questions or advanced use cases not covered in this guide, consult the component source code or reach out to the development team.