Capítulo 38 de 54

Chapter 38: Faceting (React) Guide

Core Idea

Faceting answers "which filter choices remain," not "which rows remain" (that's filtering) or "what's the summary value" (that's aggregation) — a column's faceted row model excludes that column's own filter while applying every other active filter, which is exactly what lets facets narrow each other without a facet ever hiding its own alternatives.

Key Concepts

  • Three questions, three features: filtering → which rows remain; faceting → which filter choices remain (values/counts/ranges for building filter UI); row aggregation → what summary value can be computed (sum/average/etc. for footers or grouped rows). Faceted counts never touch a column's aggregationFn.
  • The "excludes its own filter" rule: if Region=Europe and Plan=Pro are both active, the Plan facet's counts are computed from all Europe rows (region filter applied, plan filter excluded) so the user still sees every plan option; a Status facet, meanwhile, does see the plan filter applied, since it's not the Status column's own filter.
  • Setup: columnFacetingFeature + columnFilteringFeature, plus row-model factories matched to what you need: facetedRowModel: createFacetedRowModel() (required for any client-side faceting — without it, facets fall back to pre-filtered rows and stop reacting to other filters), facetedUniqueValues: createFacetedUniqueValues() (value→count maps), facetedMinMaxValues: createFacetedMinMaxValues() (numeric range).
  • Three column-level read APIs: column.getFacetedRowModel() (rows passing every filter but this column's own — for custom calculations), column.getFacetedUniqueValues() (a Map<value, count> — checkboxes, selects, autocomplete), column.getFacetedMinMaxValues() ([min, max] or undefined — range sliders/number inputs).
  • Multi-value rows: a scalar column normally contributes one value per row, so unique-value counts double as row counts. A column can contribute several facet values per row via getUniqueValues: (row) => [...] (e.g. a tags array) — then counts are occurrence counts, not row counts, unless getUniqueValues is written to emit each value at most once per row.
  • Reactivity in React: since facet reads pull off a stable column object, wrap the component rendering facet options in table.Subscribe selecting state.columnFilters (or whatever slice should trigger recompute) — otherwise it won't update when a sibling filter changes.
  • Bucketing continuous values: high-cardinality columns (dates, byte sizes, prices) are more useful faceted into buckets than as raw unique values. Use getUniqueValues to emit a bucket key while keeping the accessor's raw value for everything else, and build a matching custom filterFn (via constructFilterFn, resolveDataValue mapping the raw value to the same bucket key) so facet counts and filter results always agree — no hidden derived column needed.
  • Performance: built-in client-side faceting row models are memoized (recompute only on relevant input/filter changes), but cost still scales with rows × columns × unique values. For high-cardinality columns: render only the top-N values, let users search before listing, bucket continuous values, or move faceting server-side. Derive/sort facet option lists close to the subscribing component, not repeatedly in unrelated ones.
  • Custom server-side faceting: supply your own facetedUniqueValues/facetedMinMaxValues factories — each receives (table, columnId) once and must return a function that resolves the live result on every read (not cached by the table itself), so read from table.options.meta/a store/signal inside that returned function to reflect fresh server data, and memoize inside the factory yourself if the calculation is expensive. A server query for one column should mirror the built-in behavior: apply every other active filter, exclude that column's own.
  • Global faceting: mirrors column faceting but across every column eligible for global filtering — table.getGlobalFacetedRowModel(), getGlobalFacetedUniqueValues(), getGlobalFacetedMinMaxValues(), powered by the same factories. Requires globalFilteringFeature. Custom factories receive the special column id '__global__' for these requests, so a server-backed implementation can branch on it.

Code Examples

const features = tableFeatures({
  columnFacetingFeature, columnFilteringFeature,
  filteredRowModel: createFilteredRowModel(),
  facetedRowModel: createFacetedRowModel(),
  facetedUniqueValues: createFacetedUniqueValues(),
  facetedMinMaxValues: createFacetedMinMaxValues(),
})

// reactive facet options, local subscription
<table.Subscribe selector={(s) => s.columnFilters}>
  {() => Array.from(column.getFacetedUniqueValues()).map(([value, count]) => (
    <label key={String(value)}>{String(value)} ({count})</label>
  ))}
</table.Subscribe>
  • What it demonstrates: the table.Subscribe wrapper needed for facet options to react to sibling-filter changes, plus the unique-values-map-to-list conversion.

Key Takeaways

  1. A facet's own filter is always excluded from its own faceted row model — that's the mechanism that keeps a filter's own choices from disappearing as it's applied.
  2. Register only the row-model factories you need (facetedUniqueValues vs facetedMinMaxValues vs both) — each is independently optional.
  3. High-cardinality columns need bucketing or server-side faceting, not raw getFacetedUniqueValues() — keep the bucket definition shared between faceting (getUniqueValues) and filtering (resolveDataValue) so counts and results agree.

Connects To

  • Column Filtering (React) Guide: constructFilterFn, resolveDataValue, the filter side of the bucketing pattern.
  • Global Filtering (React) Guide: the feature global faceting builds on top of.
  • Aggregation (React) Guide: the related-but-distinct "summary value" concept faceting is often confused with.