Files
calendar/packages/features/data-table/GUIDE.md
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
fddee71829 refactor: extract bookings list and calendar views (#24486)
* refactor: extract bookings list and calendar views with nuqs state management

- Extract list-related code into BookingsListView component
- Create empty BookingsCalendarView component for future implementation
- Add nuqs query param state management for view toggle (defaults to list)
- Update bookings-listing-view to conditionally render views
- No visible changes to users (list view remains default)

Co-Authored-By: [email protected] <[email protected]>

* refactor: move data fetching logic to parent component

- Keep useFilterValue calls, trpc query, columns, flatData, bookingsToday, finalData, and table setup in parent component
- BookingsListView now receives data as props instead of fetching it
- This allows both list and calendar views to share the same data source

Co-Authored-By: [email protected] <[email protected]>

* fix: add customView column back to render booking items

The customView column was inadvertently removed during refactoring. This column is crucial as it renders the actual BookingListItem components, the "today" header, and the "next" header for the bookings list.

Co-Authored-By: [email protected] <[email protected]>

* refactor: rename files for better clarity

- Renamed bookings-listing-view.tsx → bookings-view.tsx (parent view)
- Renamed bookings-list-view.tsx → BookingsList.tsx (list component)
- Renamed bookings-calendar-view.tsx → BookingsCalendar.tsx (calendar component)
- Moved list and calendar components from views/ to components/ directory
- Updated all imports to reflect new structure

This creates a clearer hierarchy where -view is the orchestrator and components are the renderers.

Co-Authored-By: [email protected] <[email protected]>

* clean up implementation

* clean up types

* revert unnecessary changes

* Update packages/features/data-table/GUIDE.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-21 11:09:47 +00:00

34 KiB

DataTable System Guide

A comprehensive guide to using Cal.com's DataTable system for building powerful, filterable, and paginated data tables.

Table of Contents

  1. Overview
  2. Quick Start
  3. Core Components
  4. Filter System
  5. Segment System
  6. Pagination Modes
  7. Toolbar Components
  8. Advanced Usage
  9. Real-world Examples
  10. TypeScript Types Reference
  11. Best Practices

Overview

The DataTable system is a comprehensive solution for displaying tabular data with advanced features including:

  • Advanced filtering with 5 filter types and custom operators
  • Segment system for saving and sharing filter configurations
  • Traditional pagination with page-based navigation (recommended)
  • Column management (sorting, resizing, visibility)
  • Bulk actions and selection management
  • Full TypeScript support
  • Alternative infinite scroll mode (has known issues, use with caution)

Architecture

The system consists of three main layers:

  1. DataTableProvider - Context provider managing all table state
  2. DataTableWrapper - UI wrapper handling pagination, toolbars, and loading states
  3. DataTable - Core table component with optional virtualization

Data Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        DataTableProvider                        │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │   Table State   │  │  Filter State   │  │ Segment State   │  │
│  │ • pagination    │  │ • columnFilters │  │ • activeSegment │  │
│  │ • sorting       │  │ • searchTerm    │  │ • userSegments  │  │
│  │ • columnVis     │  │ • activeFilters │  │ • systemSegments│  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                       DataTableWrapper                          │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │   Toolbar       │  │   Data Table    │  │   Pagination    │  │
│  │ • SearchBar     │  │ • Columns       │  │ • PageControls  │  │
│  │ • FilterBar     │  │ • Rows          │  │ • PageSize      │  │
│  │ • Segments      │  │ • Selection     │  │ • TotalCount    │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                         Backend API                             │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │ Filter Processing│  │ Data Fetching   │  │ Response Format │  │
│  │ • makeWhereClause│  │ • Prisma Query  │  │ • data[]        │  │
│  │ • makeSqlCondition│  │ • Raw SQL      │  │ • totalCount    │  │
│  │ • Column Mapping │  │ • Pagination   │  │ • meta          │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Component Hierarchy

DataTableProvider (Context)
└── DataTableWrapper (UI Container)
    ├── Toolbar
    │   ├── ToolbarLeft
    │   │   ├── DataTableToolbar.SearchBar
    │   │   ├── DataTableFilters.FilterBar
    │   │   └── DataTableFilters.ColumnVisibilityButton
    │   └── ToolbarRight
    │       ├── DataTableFilters.ClearFiltersButton
    │       ├── DataTableSegment.SaveButton
    │       └── DataTableSegment.Select
    ├── DataTable (Core Table)
    │   ├── Table Header
    │   ├── Table Body (Virtualized/Standard)
    │   └── Selection Bar (Conditional)
    └── Pagination Controls
        ├── Page Navigation
        ├── Page Size Selector
        └── Total Count Display

Quick Start

Basic Setup

import {
  DataTableProvider,
  DataTableWrapper,
  DataTableFilters,
  useDataTable,
  ColumnFilterType,
} from "@calcom/features/data-table";

// 1. Define your data type
type User = {
  id: number;
  name: string;
  email: string;
  role: string;
};

// 2. Create columns with filtering support
const columns = [
  {
    id: "name",
    header: "Name",
    accessorKey: "name",
    meta: {
      type: ColumnFilterType.TEXT,
    },
  },
  {
    id: "role",
    header: "Role", 
    accessorKey: "role",
    meta: {
      type: ColumnFilterType.SINGLE_SELECT,
    },
  },
];

// 3. Setup the table
function UserTable() {
  const table = useReactTable({
    data: users,
    columns,
    // ... other table options
  });

  return (
    <DataTableProvider tableIdentifier="user-table">
      <DataTableWrapper
        table={table}
        paginationMode="standard"
        ToolbarLeft={
          <>
            <DataTableToolbar.SearchBar />
            <DataTableFilters.FilterBar table={table} />
          </>
        }
        ToolbarRight={
          <>
            <DataTableFilters.ClearFiltersButton />
            <DataTableSegment.SaveButton />
            <DataTableSegment.Select />
          </>
        }
      />
    </DataTableProvider>
  );
}

Core Components

DataTableProvider

The context provider that manages all table state including filters, sorting, pagination, and segments.

Props

interface DataTableProviderProps {
  tableIdentifier?: string;           // Unique identifier for the table
  children: React.ReactNode;
  useSegments?: UseSegments;          // Custom segment hook
  defaultPageSize?: number;           // Default: 10
  ctaContainerClassName?: string;     // CSS class for CTA container
  segments?: FilterSegmentOutput[];   // Provided segments
  timeZone?: string;                  // Timezone for date filters
  preferredSegmentId?: SegmentIdentifier | null;
  systemSegments?: SystemFilterSegment[];
}

Context Values

The provider exposes comprehensive state management:

type DataTableContextType = {
  // Filters
  activeFilters: ActiveFilters;
  addFilter: (columnId: string) => void;
  updateFilter: (columnId: string, value: FilterValue) => void;
  removeFilter: (columnId: string) => void;
  clearAll: (exclude?: string[]) => void;

  // Sorting
  sorting: SortingState;
  setSorting: OnChangeFn<SortingState>;

  // Column management
  columnVisibility: VisibilityState;
  setColumnVisibility: OnChangeFn<VisibilityState>;
  columnSizing: ColumnSizingState;
  setColumnSizing: OnChangeFn<ColumnSizingState>;

  // Pagination
  pageIndex: number;
  pageSize: number;
  setPageIndex: (pageIndex: number | null) => void;
  setPageSize: (pageSize: number | null) => void;
  offset: number;
  limit: number;

  // Segments
  segments: CombinedFilterSegment[];
  selectedSegment: CombinedFilterSegment | undefined;
  segmentId: SegmentIdentifier | null;
  setSegmentId: (id: SegmentIdentifier | null) => void;
  canSaveSegment: boolean;
  isSegmentEnabled: boolean;

  // Search
  searchTerm: string;
  setSearchTerm: (searchTerm: string | null) => void;
};

DataTableWrapper

The main wrapper component that handles UI concerns, pagination, and toolbar layout.

Props

type DataTableWrapperProps<TData> = {
  table: ReactTableType<TData>;
  testId?: string;
  bodyTestId?: string;
  isPending?: boolean;
  totalRowCount?: number;
  variant?: "default" | "compact";
  className?: string;
  containerClassName?: string;
  headerClassName?: string;
  rowClassName?: string;
  children?: React.ReactNode;
  tableContainerRef?: React.RefObject<HTMLDivElement>;
  onRowMouseclick?: (row: Row<TData>) => void;

  // Toolbar slots
  ToolbarLeft?: React.ReactNode;
  ToolbarRight?: React.ReactNode;
  
  // Loading states
  EmptyView?: React.ReactNode;
  LoaderView?: React.ReactNode;
} & (
  // Infinite pagination
  | {
      paginationMode: "infinite";
      hasNextPage: boolean;
      fetchNextPage: () => void;
      isFetching: boolean;
    }
  // Standard pagination  
  | {
      paginationMode: "standard";
      hasNextPage?: never;
      fetchNextPage?: never;
      isFetching?: never;
    }
);

DataTable

The core table component with column resizing and pinning support.

Key Features

  • Column resizing - Drag to resize columns
  • Column pinning - Pin columns to left/right
  • Responsive design - Adapts to mobile screens
  • Accessibility - Full keyboard navigation support
  • Optional virtualization - Available for infinite mode (use with caution)

Filter System

The DataTable supports 5 filter types with various operators and options.

Filter Types

1. Single Select Filter

{
  type: ColumnFilterType.SINGLE_SELECT,
  options: [
    { label: "Admin", value: "admin" },
    { label: "User", value: "user" },
    { label: "Guest", value: "guest" },
  ]
}

2. Multi Select Filter

{
  type: ColumnFilterType.MULTI_SELECT,
  options: [
    { label: "Engineering", value: "eng", section: "Departments" },
    { label: "Marketing", value: "marketing", section: "Departments" },
    { label: "Sales", value: "sales", section: "Departments" },
  ]
}

3. Text Filter

{
  type: ColumnFilterType.TEXT,
  textOptions: {
    allowedOperators: ["contains", "equals", "startsWith"],
    placeholder: "Search names..."
  }
}

Available operators:

  • equals - Exact match
  • notEquals - Not equal
  • contains - Contains substring
  • notContains - Does not contain
  • startsWith - Starts with
  • endsWith - Ends with
  • isEmpty - Is empty
  • isNotEmpty - Is not empty

4. Number Filter

{
  type: ColumnFilterType.NUMBER
}

Available operators:

  • eq - Equal to
  • neq - Not equal to
  • gt - Greater than
  • gte - Greater than or equal
  • lt - Less than
  • lte - Less than or equal

5. Date Range Filter

{
  type: ColumnFilterType.DATE_RANGE,
  dateRangeOptions: {
    range: "past", // or "custom"
    convertToTimeZone: true
  }
}

Column Filter Configuration

Add filtering to columns using the meta property:

const columns = [
  {
    id: "status",
    header: "Status",
    accessorKey: "status",
    meta: {
      type: ColumnFilterType.SINGLE_SELECT,
      icon: "circle-dot", // Optional icon
    } as ColumnFilterMeta,
  },
  {
    id: "createdAt",
    header: "Created",
    accessorKey: "createdAt",
    meta: {
      type: ColumnFilterType.DATE_RANGE,
      dateRangeOptions: {
        range: "past",
        convertToTimeZone: true,
      },
    } as ColumnFilterMeta,
  },
];

Faceted Filters with getFacetedUniqueValues

For select filters that need dynamic options based on data, use getFacetedUniqueValues in your table configuration:

// In your table configuration
const table = useReactTable({
  // ... other options
  getFacetedUniqueValues: (_, columnId) => () => {
    switch (columnId) {
      case "teamId":
        return convertFacetedValuesToMap(
          teams.map(team => ({
            label: team.name,
            value: team.id
          }))
        );
      case "role":
        return convertFacetedValuesToMap([
          { label: "Admin", value: "admin" },
          { label: "Member", value: "member" }
        ]);
      default:
        return new Map();
    }
  }
});

Custom Faceted Values Hook Example:

From apps/web/modules/bookings/hooks/useFacetedUniqueValues.ts:

export function useFacetedUniqueValues() {
  const eventTypes = useEventTypes();
  const { data: teams } = trpc.viewer.teams.list.useQuery();
  const { data: members } = trpc.viewer.teams.listSimpleMembers.useQuery();
  
  return useCallback(
    (_: Table<any>, columnId: string) => (): Map<FacetedValue, number> => {
      if (columnId === "eventTypeId") {
        return convertFacetedValuesToMap(eventTypes || []);
      } else if (columnId === "teamId") {
        return convertFacetedValuesToMap(
          (teams || []).map((team) => ({
            label: team.name,
            value: team.id,
          }))
        );
      } else if (columnId === "userId") {
        return convertFacetedValuesToMap(
          (members || [])
            .map((member) => ({
              label: member.name,
              value: member.id,
            }))
            .filter((option): option is { label: string; value: number } => Boolean(option.label))
        );
      }
      return new Map<FacetedValue, number>();
    },
    [eventTypes, teams, members]
  );
}

Usage in Table Configuration:

// From packages/features/users/components/UserTable/UserListTable.tsx
const table = useReactTable({
  // ... other options
  getFacetedUniqueValues: (_, columnId) => () => {
    if (facetedTeamValues) {
      switch (columnId) {
        case "role":
          return convertFacetedValuesToMap(facetedTeamValues.roles);
        case "teamId":
          return convertFacetedValuesToMap(facetedTeamValues.teams);
        default:
          return new Map();
      }
    }
    return new Map();
  }
});

Filter Components

FilterBar

Displays active filters and add filter button:

<DataTableFilters.FilterBar table={table} />

Individual Filter Components

// Add filter button
<DataTableFilters.AddFilterButton table={table} />

// Active filters display
<DataTableFilters.ActiveFilters table={table} />

// Clear all filters
<DataTableFilters.ClearFiltersButton />

// Column visibility toggle
<DataTableFilters.ColumnVisibilityButton table={table} />

Segment System

Segments allow users to save and share filter configurations. There are two types:

Important: Filter segments are only enabled when you pass the useSegments prop to DataTableProvider:

<DataTableProvider
  tableIdentifier="users"
  useSegments={useSegments} // Required to enable segments
>
  {/* Your table content */}
</DataTableProvider>

Without the useSegments prop, segment functionality will be disabled and segment-related components will not be available.

System Segments

Predefined segments created by developers:

const systemSegments: SystemFilterSegment[] = [
  {
    id: "active-users",
    name: "Active Users",
    type: "system",
    activeFilters: [
      {
        f: "status",
        v: {
          type: ColumnFilterType.SINGLE_SELECT,
          data: "active"
        }
      }
    ],
    sorting: [{ id: "lastLogin", desc: true }],
  }
];

<DataTableProvider 
  systemSegments={systemSegments}
  tableIdentifier="user-table"
>
  {/* ... */}
</DataTableProvider>

User Segments

Segments saved by users with personal or team scope:

// Personal segment (scope: "USER")
// Team segment (scope: "TEAM")

Segment Components

// Segment selector dropdown
<DataTableSegment.Select />

// Save current state as segment
<DataTableSegment.SaveButton />

// Segment management (rename, duplicate, delete)
// Available in the segment dropdown submenu

Segment Permissions

  • Personal segments: Only visible to the creator
  • Team segments: Visible to all team members
  • System segments: Visible to all users
  • Admin actions: Rename/delete require admin/owner permissions

Pagination Modes

Traditional page-based pagination is the recommended approach:

<DataTableWrapper
  paginationMode="standard"
  totalRowCount={totalCount}
  table={table}
/>

Features:

  • Page numbers and navigation
  • Configurable page sizes
  • Total count display
  • Reliable performance
  • Better user experience
  • No virtualization issues

Why Standard Mode is Preferred: Standard pagination provides a more predictable and stable user experience. It avoids the complexity and potential issues associated with virtualized infinite scrolling.

Infinite Pagination (Use with Caution)

Alternative infinite scroll mode with known limitations:

<DataTableWrapper
  paginationMode="infinite"
  hasNextPage={hasNextPage}
  fetchNextPage={fetchNextPage}
  isFetching={isFetching}
  table={table}
/>

Features:

  • Automatic loading on scroll
  • Virtualized rendering
  • Fixed container height (80dvh)

Known Issues:

  • Virtualized infinite loading has several problems
  • Can cause performance and UX issues
  • Standard mode was introduced to address these problems
  • Use only when absolutely necessary and with thorough testing

Toolbar Components

DataTableToolbar

Container and utility components for table toolbars:

// Toolbar container
<DataTableToolbar.Root>
  <DataTableToolbar.SearchBar />
  <DataTableToolbar.CTA color="primary" StartIcon="plus">
    Add User
  </DataTableToolbar.CTA>
</DataTableToolbar.Root>

// Search input with debounced updates
<DataTableToolbar.SearchBar className="max-w-48" />

// Clear filters button (auto-hides when no filters)
<DataTableToolbar.ClearFiltersButton />

// Custom action button
<DataTableToolbar.CTA 
  color="secondary" 
  StartIcon="download"
  onClick={handleExport}
>
  Export
</DataTableToolbar.CTA>

DataTableSelectionBar

For bulk actions when rows are selected:

{numberOfSelectedRows > 0 && (
  <DataTableSelectionBar.Root>
    <p>{t("number_selected", { count: numberOfSelectedRows })}</p>
    
    <DataTableSelectionBar.Button
      color="destructive"
      icon="trash-2"
      onClick={handleBulkDelete}
    >
      Delete Selected
    </DataTableSelectionBar.Button>
  </DataTableSelectionBar.Root>
)}

Advanced Usage

Server-Side Operations

The DataTable system is designed for server-side filtering, sorting, and pagination with standard pagination. This approach is necessary because we only fetch a limited number of items per page, unlike the previous infinite scrolling approach that cached large amounts of data and could filter on the client side.

Basic Server-Side Pattern

// Get current table state for API calls
const { limit, offset, sorting } = useDataTable();
const columnFilters = useColumnFilters();

// Use in your API call
const { data } = trpc.users.list.useQuery({
  limit,
  offset,
  sorting,
  filters: columnFilters
});

Prisma Where Clause Construction

For Prisma-based backends, extract individual filters and build typed where conditions:

// From packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
const roleFilter = filters.find((filter) => filter.id === "role") as
  | TypedColumnFilter<ColumnFilterType.MULTI_SELECT>
  | undefined;
const teamFilter = filters.find((filter) => filter.id === "teams") as
  | TypedColumnFilter<ColumnFilterType.MULTI_SELECT>
  | undefined;
const lastActiveAtFilter = filters.find((filter) => filter.id === "lastActiveAt") as
  | TypedColumnFilter<ColumnFilterType.DATE_RANGE>
  | undefined;

const whereClause: Prisma.MembershipWhereInput = {
  user: {
    ...(teamFilter && {
      teams: {
        some: {
          team: makeWhereClause({
            columnName: "name",
            filterValue: teamFilter.value,
          }),
        },
      },
    }),
    ...(lastActiveAtFilter &&
      makeWhereClause({
        columnName: "lastActiveAt",
        filterValue: lastActiveAtFilter.value,
      })),
  },
  teamId: organizationId,
  ...(roleFilter && makeWhereClause({
    columnName: "role",
    filterValue: roleFilter.value,
  })),
};

Raw SQL Optimization

For performance-critical queries, use raw SQL with makeSqlCondition:

// From packages/lib/server/service/InsightsRoutingBaseService.ts
async getFilterConditions(): Promise<Prisma.Sql | null> {
  const conditions: Prisma.Sql[] = [];
  const columnFilters = this.filters.columnFilters || [];
  
  // Convert columnFilters array to object for easier access
  const filtersMap = columnFilters.reduce((acc, filter) => {
    acc[filter.id] = filter;
    return acc;
  }, {} as Record<string, TypedColumnFilter<ColumnFilterType>>);

  // Extract booking status order filter
  const bookingStatusOrder = filtersMap["bookingStatusOrder"];
  if (bookingStatusOrder && isMultiSelectFilterValue(bookingStatusOrder.value)) {
    const statusCondition = makeSqlCondition(bookingStatusOrder.value);
    if (statusCondition) {
      conditions.push(Prisma.sql`rfrd."bookingStatusOrder" ${statusCondition}`);
    }
  }

  // Extract booking UID filter
  const bookingUid = filtersMap["bookingUid"];
  if (bookingUid && isTextFilterValue(bookingUid.value)) {
    const uidCondition = makeSqlCondition(bookingUid.value);
    if (uidCondition) {
      conditions.push(Prisma.sql`rfrd."bookingUid" ${uidCondition}`);
    }
  }

  // Join all conditions with AND
  return conditions.reduce((acc, condition, index) => {
    if (index === 0) return condition;
    return Prisma.sql`(${acc}) AND (${condition})`;
  });
}

Advanced Parameter Manipulation

For complex cases where you need to manipulate filter data before sending to the backend, extract the logic into a separate hook:

// packages/features/insights/hooks/useInsightsRoutingParameters.ts
export function useInsightsRoutingParameters() {
  const { scope, selectedTeamId } = useInsightsOrgTeams();
  
  // Get date range filter and manipulate it
  const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data;
  const startDate = useChangeTimeZoneWithPreservedLocalTime(
    useMemo(() => {
      return dayjs(createdAtRange?.startDate ?? getDefaultStartDate().toISOString())
        .startOf("day")
        .toISOString();
    }, [createdAtRange?.startDate])
  );
  
  // Get other column filters excluding the manipulated ones
  const columnFilters = useColumnFilters({
    exclude: ["createdAt"],
  });

  return {
    scope,
    selectedTeamId,
    startDate,
    endDate,
    columnFilters,
  };
}

Key Hooks for Server-Side Integration

  • useColumnFilters() - Get applied filters for backend requests
  • useDataTable() - Get limit, offset, sorting for pagination
  • useFilterValue(columnId, schema) - Get specific filter value with validation
  • Custom parameter hooks - Extract complex manipulation logic

Server Utility Functions

The DataTable system provides utility functions for both Prisma and raw SQL approaches:

  • makeWhereClause() - Converts filter values to Prisma where clause objects
  • makeSqlCondition() - Converts filter values to raw SQL conditions
  • makeOrderBy() - Converts sorting state to Prisma orderBy format

Custom Hooks

useDataTable

Access the DataTable context:

const {
  activeFilters,
  sorting,
  columnVisibility,
  pageIndex,
  pageSize,
  searchTerm,
  selectedSegment,
} = useDataTable();

useColumnFilters

Get processed column filters for API calls:

const columnFilters = useColumnFilters();
// Returns: ColumnFilter[] ready for backend consumption

// With exclusions (useful when manipulating specific filters)
const columnFilters = useColumnFilters({
  exclude: ["createdAt", "dateRange"]
});

useFilterableColumns

Extract filterable columns from table definition:

const filterableColumns = useFilterableColumns(table);

Custom Filter Components

Create custom filter implementations:

function CustomStatusFilter({ column }: { column: Column<any> }) {
  const { updateFilter } = useDataTable();
  
  return (
    <Select
      onValueChange={(value) => 
        updateFilter(column.id, {
          type: ColumnFilterType.SINGLE_SELECT,
          data: value
        })
      }
    >
      {/* Custom filter UI */}
    </Select>
  );
}

Portal Integration

Use portals for toolbar actions:

const { ctaContainerRef } = useDataTable();

{ctaContainerRef.current && createPortal(
  <div className="flex gap-2">
    <Button>Custom Action</Button>
  </div>,
  ctaContainerRef.current
)}

Real-world Examples

Example 1: User Management Table

From packages/features/users/components/UserTable/UserListTable.tsx:

<DataTableWrapper<UserTableUser>
  testId="user-list-data-table"
  table={table}
  isPending={isPending}
  totalRowCount={data?.meta?.totalRowCount}
  paginationMode="standard"
  ToolbarLeft={
    <>
      <DataTableToolbar.SearchBar />
      <DataTableFilters.ColumnVisibilityButton table={table} />
      <DataTableFilters.FilterBar table={table} />
    </>
  }
  ToolbarRight={
    <>
      <DataTableFilters.ClearFiltersButton />
      <DataTableSegment.SaveButton />
      <DataTableSegment.Select />
    </>
  }
>
  {/* Selection bar for bulk actions */}
  {numberOfSelectedRows > 0 && (
    <DataTableSelectionBar.Root>
      <p>{t("number_selected", { count: numberOfSelectedRows })}</p>
      <DeleteBulkUsers
        users={table.getSelectedRowModel().flatRows.map(row => row.original)}
        onRemove={() => table.toggleAllPageRowsSelected(false)}
      />
    </DataTableSelectionBar.Root>
  )}
</DataTableWrapper>

Example 2: Bookings List

From apps/web/modules/bookings/components/BookingsList.tsx:

<DataTableWrapper
  className="mb-6"
  tableContainerRef={tableContainerRef}
  table={table}
  testId={`${status}-bookings`}
  bodyTestId="bookings"
  headerClassName="hidden"
  isPending={query.isPending}
  totalRowCount={query.data?.totalCount}
  variant="compact"
  paginationMode="standard"
  ToolbarLeft={
    <DataTableFilters.FilterBar table={table} />
  }
  ToolbarRight={
    <>
      <DataTableFilters.ClearFiltersButton />
      <DataTableSegment.SaveButton />
      <DataTableSegment.Select />
    </>
  }
  LoaderView={<SkeletonLoader />}
  EmptyView={
    <EmptyScreen
      Icon="calendar"
      headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
      description={t("no_status_bookings_yet_description")}
    />
  }
/>

Example 3: Team Member List with Infinite Scroll

From packages/features/ee/teams/components/MemberList.tsx:

<DataTableWrapper
  testId="team-member-list-container"
  table={table}
  tableContainerRef={tableContainerRef}
  isPending={isPending}
  paginationMode="infinite"
  hasNextPage={hasNextPage}
  fetchNextPage={fetchNextPage}
  isFetching={isFetching}
  ToolbarLeft={
    <>
      <DataTableToolbar.SearchBar />
      <DataTableFilters.FilterBar table={table} />
    </>
  }
  ToolbarRight={
    <>
      <DataTableFilters.ClearFiltersButton />
      <DataTableSegment.SaveButton />
      <DataTableSegment.Select />
    </>
  }
>
  {/* Bulk selection and actions */}
  {numberOfSelectedRows > 0 && (
    <DataTableSelectionBar.Root>
      <TeamListBulkAction table={table} />
      <MassAssignAttributesBulkAction table={table} filters={columnFilters} />
    </DataTableSelectionBar.Root>
  )}
</DataTableWrapper>

TypeScript Types Reference

Core Types

// Filter value types
type FilterValue = 
  | SingleSelectFilterValue
  | MultiSelectFilterValue  
  | TextFilterValue
  | NumberFilterValue
  | DateRangeFilterValue;

// Active filter structure
type ActiveFilter = {
  f: string;           // field/column ID
  v?: FilterValue;     // filter value
};

// Segment types
type SegmentIdentifier = 
  | { id: string; type: "system" }
  | { id: number; type: "user" };

// Column filter metadata
type ColumnFilterMeta = {
  type: ColumnFilterType;
  icon?: IconName;
  dateRangeOptions?: DateRangeFilterOptions;
  textOptions?: TextFilterOptions;
};

Filter Value Schemas

// Single select
type SingleSelectFilterValue = {
  type: ColumnFilterType.SINGLE_SELECT;
  data: string | number;
};

// Multi select
type MultiSelectFilterValue = {
  type: ColumnFilterType.MULTI_SELECT;
  data: Array<string | number>;
};

// Text filter
type TextFilterValue = {
  type: ColumnFilterType.TEXT;
  data: {
    operator: TextFilterOperator;
    operand: string;
  };
};

// Number filter
type NumberFilterValue = {
  type: ColumnFilterType.NUMBER;
  data: {
    operator: NumberFilterOperator;
    operand: number;
  };
};

// Date range filter
type DateRangeFilterValue = {
  type: ColumnFilterType.DATE_RANGE;
  data: {
    startDate: string | null;
    endDate: string | null;
    preset: string;
  };
};

Segment Types

// System segment (predefined)
type SystemFilterSegment = {
  id: string;
  name: string;
  type: "system";
  activeFilters: ActiveFilters;
  sorting?: SortingState;
  columnVisibility?: Record<string, boolean>;
  columnSizing?: Record<string, number>;
  perPage?: number;
  searchTerm?: string | null;
};

// User segment (saved by users)
type UserFilterSegment = FilterSegmentOutput & {
  type: "user";
};

// Combined segment type
type CombinedFilterSegment = SystemFilterSegmentInternal | UserFilterSegment;

Best Practices

Performance

  1. Use standard pagination for most use cases (recommended)
  2. Implement proper memoization for column definitions
  3. Debounce search inputs (handled automatically by DataTableToolbar.SearchBar)
  4. Consider infinite mode only for specific cases where traditional pagination isn't suitable, but be aware of potential issues

State Management

  1. Provide unique tableIdentifier for each table instance
  2. Use segments for complex filter combinations
  3. Persist user preferences via the segment system
  4. Clear filters appropriately when changing contexts

User Experience

  1. Provide loading states with LoaderView and EmptyView
  2. Use standard pagination for consistent user experience
  3. Include search functionality for text-heavy data
  4. Implement bulk actions for management interfaces
  5. Show filter counts and active filter indicators

Accessibility

  1. Use semantic HTML (handled by components)
  2. Provide proper ARIA labels for custom filters
  3. Support keyboard navigation (built-in)
  4. Test with screen readers

Code Organization

// ✅ Good: Memoize column definitions
const columns = useMemo(() => [
  {
    id: "name",
    header: "Name",
    accessorKey: "name",
    meta: { type: ColumnFilterType.TEXT },
  },
], []);

// ✅ Good: Extract filter logic
const useUserFilters = () => {
  const columnFilters = useColumnFilters();
  return useMemo(() => 
    transformFiltersForAPI(columnFilters), 
    [columnFilters]
  );
};

// ✅ Good: Separate concerns
function UserTableContainer() {
  const filters = useUserFilters();
  const { data, isPending } = useUsers(filters);
  
  return (
    <DataTableProvider tableIdentifier="users">
      <UserTable data={data} isPending={isPending} />
    </DataTableProvider>
  );
}

Common Patterns

Filter Integration with API

function useTableData() {
  const columnFilters = useColumnFilters();
  const { sorting, pageIndex, pageSize, searchTerm } = useDataTable();
  
  const queryParams = useMemo(() => ({
    filters: columnFilters,
    sorting,
    page: pageIndex,
    limit: pageSize,
    search: searchTerm,
  }), [columnFilters, sorting, pageIndex, pageSize, searchTerm]);
  
  return useQuery({
    queryKey: ['table-data', queryParams],
    queryFn: () => fetchData(queryParams),
  });
}

Custom Filter Options

function useStatusFilterOptions() {
  const { data: statuses } = useStatuses();
  
  return useMemo(() => 
    statuses?.map(status => ({
      label: status.name,
      value: status.id,
      section: status.category,
    })) || [],
    [statuses]
  );
}

Segment Presets

const BOOKING_SEGMENTS: SystemFilterSegment[] = [
  {
    id: "upcoming",
    name: "Upcoming",
    type: "system",
    activeFilters: [
      {
        f: "status",
        v: { type: ColumnFilterType.SINGLE_SELECT, data: "confirmed" }
      },
      {
        f: "startTime",
        v: { 
          type: ColumnFilterType.DATE_RANGE, 
          data: { preset: "future", startDate: null, endDate: null }
        }
      }
    ],
    sorting: [{ id: "startTime", desc: false }],
  },
];

This guide covers the complete DataTable system. For specific implementation details, refer to the source files in packages/features/data-table/ and the usage examples throughout the Cal.com codebase.