Capítulo 35 de 54

Chapter 35: Column Filtering (React) Guide

Core Idea

Column filtering applies per-column, named filterFns against ColumnFiltersState (an array, so multiple column filters compose); v9 ships 18 built-in filter functions and a constructFilterFn helper for building/deriving your own from a value comparator plus optional value-normalization hooks.

Key Concepts

  • Setup: register columnFilteringFeature + (for client-side) filteredRowModel: createFilteredRowModel() + only the filterFns you actually use (registering the full built-in registry ships every built-in implementation). For server-side, set manualFiltering: true and skip the row model — data is assumed pre-filtered.
  • State shape: ColumnFiltersState = Array<{ id: string; value: unknown }> — an array, not a map, so several columns can be filtered simultaneously.
  • Reading: table.state.columnFilters (reactive) or table.atoms.columnFilters.get() (snapshot, no subscription). Ownership: external atom via atoms.columnFilters (v9-recommended, e.g. feeding a server query key) or classic state.columnFilters + onColumnFiltersChange; or initialState.columnFilters alone if you never need it externally — never both initialState and state for the same slice.
  • 18 built-in filterFns, grouped by shape: string — includesString/includesStringSensitive, startsWith, endsWith, equalsString/equalsStringSensitive; general equality — equals (===), weakEquals (==); emptiness — empty/notEmpty (filter value is an on/off flag, tests nullish-or-whitespace); array-valued — arrIncludes (row value includes any filter value, works on arrays or strings), arrIncludesAll, arrIncludesSome, arrHas (scalar row value equals any filter value); ranges — inNumberRange (inclusive, endpoints auto-normalized/swapped), inDateRange (inclusive, accepts Date/timestamp/date-string, blank endpoints open-ended), between (exclusive), betweenInclusive (inclusive, both range-style with open-ended blanks).
  • Custom filter functions via constructFilterFn({ filter, resolveFilterValue?, resolveDataValue?, autoRemove? }): filter(dataValue, filterValue) is the actual comparator; resolveFilterValue normalizes the filter input once per filter application (not per row — the right place for expensive prep); resolveDataValue normalizes each row's value before comparison (honored by every built-in filter, since they're all built with constructFilterFn); autoRemove(value) returns true when that filter value should be dropped from state entirely (authoritative when provided — an undefined value always clears regardless). A function built this way carries its definition, so you can spread and override an existing one to derive a variant (e.g. a diacritic-insensitive version of includesString that only overrides the two resolve* hooks, reusing the original comparator/autoRemove).
  • Signature for a plain custom filterFn (inline or registered): (row, columnId, filterValue, addMeta?) => boolean — return true to keep the row.
  • String-name references (filterFn: 'myCustomFilterFn') only type-check if registered in the filterFns slot on tableFeatures; skip registration entirely by passing the function directly to the column's filterFn option instead.
  • Disabling: enableColumnFilter: false per-column, enableColumnFilters: false table-wide, or enableFilters: false to kill both column and global filtering at once.
  • Sub-row behavior (with expanding/grouping): default is filter-from-root-down (a filtered-out parent removes all its children too — most performant, right when users should only search top-level rows). filterFromLeafRows: true inverts this — filtering runs leaf-up, so a parent survives if any descendant matches. maxLeafRowFilterDepth: 0 restricts filtering to root rows only, leaving all sub-rows unfiltered (useful to keep a parent's children intact once the parent itself passes).
  • Auto-reset interaction: the client-side filtered row model triggers the same page-index auto-reset hook covered in the Client-Side vs Server-Side Guide — if filtering is manual and the row model is skipped, that hook never fires, so reset pagination by hand in the filter change handler.

Code Examples

const startsWithFilterFn = constructFilterFn({
  filter: (dataValue, filterValue) => Boolean(dataValue?.startsWith(filterValue)),
  resolveFilterValue: (v) => String(v).toLowerCase().trim(),
  resolveDataValue: (v) => String(v ?? '').toLowerCase(),
  autoRemove: (v) => !v,
})

// deriving a diacritic-insensitive variant by overriding only the resolvers
const normalize = (v: unknown) => String(v ?? '').toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '')
const includesStringIgnoreDiacritics = constructFilterFn({
  ...filterFn_includesString,
  resolveFilterValue: normalize,
  resolveDataValue: normalize,
})
  • What it demonstrates: building a filter from scratch vs. deriving one by spreading an existing constructFilterFn-built function and overriding just the normalization hooks.

Key Takeaways

  1. Register only the built-in filter functions actually used, under their conventional keys — spreading the full registry defeats tree-shaking.
  2. resolveFilterValue runs once per filter, not per row — put expensive input normalization there, not in filter itself.
  3. For nested/grouped data, pick filterFromLeafRows/maxLeafRowFilterDepth deliberately — the default (filter-from-root) silently drops matching children under a non-matching parent.
  4. Under manual filtering, reset pagination by hand in the filter change handler — the automatic page-reset hook doesn't fire when the client-side row model is skipped.

Connects To

  • Global Filtering (React) Guide: the cross-column sibling of this feature.
  • Fuzzy Filtering (React) Guide: a constructFilterFn-style custom filter built around ranking metadata.
  • Table and Column Meta Guide: filterMeta, for filter functions that need to hand data to a paired sort function.