Skip to content

DataTable Component Documentation

A comprehensive guide to understanding and implementing the DataTable component in the Delta project.

Table of Contents

  1. Overview
  2. Architecture
  3. Core Concepts
  4. Component Structure
  5. Implementation Guide
  6. Features
  7. Advanced Usage
  8. Performance Considerations
  9. Testing
  10. Common Patterns
  11. API Reference
  12. 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:

  1. Context-Based State Management: All state is managed through a central context
  2. Composition Over Inheritance: Features are composed, not inherited
  3. Separation of Concerns: Logic, presentation, and state are clearly separated
  4. Hook-Based Logic: Business logic is encapsulated in custom hooks
  5. 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 instance
  • useDataTableFeatures: Parses and normalizes feature configuration
  • useDataTableDragDrop: Handles drag-and-drop functionality
  • useDataTableVirtualization: Manages virtualization state
  • useEnhancedColumns: Enhances columns with special features
  • useRowGrouping: 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:

  1. Default Behavior: All columns use the custom 'operator' filter function by default
  2. Custom Operator Filter: Provides type-aware filtering with multiple operators
  3. 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-click attribute
  • Clicking on drag handles (data-drag-handle attribute)
  • 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 onRowClick and 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 bulkActions are 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

  1. Cell Mode (default): Edit individual cells one at a time
  2. Row Mode: Edit entire rows at once

Save Strategies

  1. Immediate Mode: Changes are saved immediately when a cell loses focus
  2. 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
  1. External State: The data array is managed outside the DataTable
  2. Change Tracking: DataTable tracks which cells have been edited
  3. Pending Changes: All edits are collected until explicitly saved
  4. 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
  1. Always check for null: The ref might not be available immediately
  2. Use optional chaining: tableRef.current?.method()
  3. Avoid direct state manipulation: Use provided methods
  4. 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.memo for 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 useRealtime hook 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,
        }}
      />
    </>
  );
}

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 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.memo for 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

  1. Enable Debug Mode:
<DataTable debug={true} ... />
  1. Check Context Values:
const Component = () => {
  const context = useDataTableContext();
  console.log('Table State:', context.table.getState());
};
  1. Monitor Performance:
// Use React DevTools Profiler
// Check render counts and duration
  1. Validate Props:
// DataTable will warn about invalid feature combinations
// Check console for warnings

Best Practices

  1. Always memoize columns to prevent unnecessary re-renders
  2. Use proper TypeScript types for data and column definitions
  3. Enable features progressively - start simple, add features as needed
  4. Test with realistic data volumes to ensure performance
  5. Provide meaningful empty states for better UX
  6. Use semantic HTML and ARIA labels for accessibility
  7. Implement proper error boundaries for error handling
  8. Consider mobile experience - use responsive features
  9. Document custom renderers for team understanding
  10. 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.