Filtering

Compose DataTable filtering with the general Filters composite and TanStack adapters.

Overview

DataTable filtering is composed from normal Cubitt surfaces:

  • defineFilters(...) describes the filter fields
  • Filters renders the UI
  • useFiltersTanstack(...) adapts those filters to TanStack Table
  • DataTable receives the adapted columns, columnFilters, and change handlers

This is the same pattern as Search and Column Visibility: the table stays focused on rendering, while surrounding state and UI stay composable.

How The Composition Works

useFiltersTanstack(...) gives you the pieces that bridge Filters and DataTable:

  • filters and handleFiltersChange for the Filters component
  • filterFields which may differ from the original definition after runtime option resolution
  • columns, columnFilters, and onColumnFiltersChange for DataTable

That means the recommended flow is:

  1. define fields with defineFilters(...)
  2. pass the definition plus your table columns to useFiltersTanstack(...)
  3. render Filters with the returned filter state
  4. render DataTable with the returned TanStack table state

Derived Select Options

When strategy="client" and a select or multiselect field does not provide options, useFiltersTanstack(...) derives them from data.

const filterDefs = defineFilters([
  {
    key: "status",
    label: "Status",
    type: "select",
  },
  {
    key: "department",
    label: "Department",
    type: "select",
  },
] as const);

const { filterFields } = useFiltersTanstack({
  definition: filterDefs,
  columns,
  data,
});

Static options still win when you provide them explicitly.

If you need custom labels, icons, or sorting, use optionResolvers:

import {
  deriveFilterOptions,
  useFiltersTanstack,
} from "@tilt-legal/cubitt-components/filters";

const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns,
  data,
  optionResolvers: {
    assignee: ({ data, getFieldValue }) =>
      deriveFilterOptions({
        data,
        getValue: getFieldValue,
        mapOption: (user) => ({
          value: user.id,
          label: user.name,
          icon: <Avatar>{user.initials}</Avatar>,
        }),
        sort: (a, b) => a.label.localeCompare(b.label),
      }),
  },
});

Mapping Filter Fields To Columns

Use fieldColumnMap when the filter key is not the same as the TanStack column id.

const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns,
  data,
  fieldColumnMap: {
    search: "name",
  },
});

This is useful for synthetic fields like a single search filter that targets one column id in TanStack Table.

Searching Across Multiple Values

Use searchResolvers when one filter field should inspect multiple row values.

const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns,
  data,
  searchResolvers: {
    search: (row) => [row.name, row.email, row.department],
  },
});

This keeps broad text search inside the same structured filter model instead of wiring a separate global filter.

Client And Server Flows

Client-Side

Use strategy="client" with data when filtering should happen in the browser.

const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns,
  data,
  strategy: "client",
});

Pass the returned columnFilters, columns, and onColumnFiltersChange into DataTable with enableFiltering.

Server-Side

Use Filters for the UI, but send filters to your backend and render the returned rows.

const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns,
  strategy: "server",
});

useEffect(() => {
  fetchRows(tableFilters.filters).then(setRows);
}, [tableFilters.filters]);

In that mode, Filters still owns the structured filter state, but your server decides what rows come back.

Legacy API

useDataTableFilters and DataTableFilter are deprecated. They still exist for legacy consumers, but the recommended path is the composed Filters plus useFiltersTanstack(...) pattern shown on this page.

API Reference

useFiltersTanstack(...) Options

OptionTypeDefaultDescription
definitionFilterDefinitionResultrequiredTyped filter definition created with defineFilters(...)
columnsColumnDef<TData>[]requiredBase TanStack columns
datareadonly TData[]-Client-side rows for local filtering and option derivation
strategy"client" | "server""client"Whether filtering happens locally or externally
defaultFiltersFilter[][]Initial uncontrolled filters
filtersFilter[]-Controlled filters
onFiltersChange(filters) => void-Controlled filters change handler
fieldColumnMapRecord<string, string>-Map filter keys to TanStack column ids
mapFieldToColumnId(fieldKey) => string | undefined-Dynamic column-id resolver
searchResolversRecord<string, (row) => unknown | unknown[]>-Search one filter across multiple row values
fieldAccessorsRecord<string, (row) => unknown | unknown[]>-Override value access for filter evaluation
filterOverridesRecord<string, FilterOverrideFn<TData>>-Override filter evaluation for specific fields
optionResolversRecord<string, FilterOptionResolver<TData>>-Derive custom option lists from runtime data
overrideExistingFilterFnbooleanfalseReplace existing TanStack column filterFns

Return Value

FieldTypeDescription
filtersFilter[]Current structured filter state
setFiltersDispatch<SetStateAction<Filter[]>>Imperative filter state setter
handleFiltersChange(filters: Filter[]) => voidChange handler for Filters
filterFieldsFilterFieldsConfigResolved fields to pass to Filters
columnsColumnDef<TData>[]TanStack columns with filter adapters applied
columnFiltersColumnFiltersStateDerived TanStack column-filter state
onColumnFiltersChangeOnChangeFn<ColumnFiltersState>Adapter back from TanStack updates
filteredRowsTData[] | undefinedClient-side filtered rows when strategy="client"
filteredRowCountnumber | undefinedCount for the filtered client-side result

On this page