* refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier - Remove Next.js usePathname dependency from DataTableProvider - Make tableIdentifier a required prop instead of optional - Update all usages to provide explicit tableIdentifier values - This makes DataTableProvider usable in non-Next.js contexts Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: use usePathname at usage sites instead of hardcoding tableIdentifier - Add validation in DataTableProvider for empty/nullish tableIdentifier - Use usePathname() in Next.js apps to pass pathname as tableIdentifier - Use descriptive identifiers for non-Next.js package components - This keeps DataTableProvider framework-agnostic while allowing Next.js apps to use pathname Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * use pathname instead of hard-coded identifiers * change type of tableIdentifier * simplify --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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
- Overview
- Quick Start
- Core Components
- Filter System
- Segment System
- Pagination Modes
- Toolbar Components
- Advanced Usage
- Real-world Examples
- TypeScript Types Reference
- 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:
- DataTableProvider - Context provider managing all table state
- DataTableWrapper - UI wrapper handling pagination, toolbars, and loading states
- 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
});
const pathname = usePathname();
const tableIdentifier = "hard-coded idenfidier" // or pathname;
return (
<DataTableProvider tableIdentifier={tableIdentifier}>
<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 (throws if empty)
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 matchnotEquals- Not equalcontains- Contains substringnotContains- Does not containstartsWith- Starts withendsWith- Ends withisEmpty- Is emptyisNotEmpty- Is not empty
4. Number Filter
{
type: ColumnFilterType.NUMBER
}
Available operators:
eq- Equal toneq- Not equal togt- Greater thangte- Greater than or equallt- Less thanlte- 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
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}
>
{/* ... */}
</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
Standard Pagination (Recommended)
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 requestsuseDataTable()- Getlimit,offset,sortingfor paginationuseFilterValue(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 objectsmakeSqlCondition()- Converts filter values to raw SQL conditionsmakeOrderBy()- 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
- Use standard pagination for most use cases (recommended)
- Implement proper memoization for column definitions
- Debounce search inputs (handled automatically by DataTableToolbar.SearchBar)
- Consider infinite mode only for specific cases where traditional pagination isn't suitable, but be aware of potential issues
State Management
- Provide unique tableIdentifier for each table instance
- Use segments for complex filter combinations
- Persist user preferences via the segment system
- Clear filters appropriately when changing contexts
User Experience
- Provide loading states with LoaderView and EmptyView
- Use standard pagination for consistent user experience
- Include search functionality for text-heavy data
- Implement bulk actions for management interfaces
- Show filter counts and active filter indicators
Accessibility
- Use semantic HTML (handled by components)
- Provide proper ARIA labels for custom filters
- Support keyboard navigation (built-in)
- 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>
<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.