Filter State Shape
A normalized ActiveFilter[] array as the single source of truth for multi-select and date-range filters — the toolbar, the removable chips row, and TanStack Table's columnFilters all read from (and write to) the same shape.
npx shadcn add https://shad-table.dev/r/filter-state-shape-table.json| Title | Category | Priority | Status | Due date |
|---|---|---|---|---|
| Redesign onboarding flow | Design | High | In Progress | Aug 5, 2026 |
| Fix pagination bug on export | Engineering | High | Open | Aug 8, 2026 |
| Write Q3 newsletter draft | Marketing | Low | Done | Jul 28, 2026 |
| Audit component color tokens | Design | Medium | Open | Aug 15, 2026 |
| Migrate auth to new session store | Engineering | High | In Progress | Aug 20, 2026 |
| Plan launch landing page copy | Marketing | Medium | Open | Aug 1, 2026 |
| Ship dark mode for settings page | Design | Medium | Done | Jul 22, 2026 |
| Add rate limiting to public API | Engineering | High | Open | Aug 25, 2026 |
| Set up A/B test for pricing page | Marketing | Low | In Progress | Aug 12, 2026 |
| Review accessibility on data table | Design | Medium | Open | Aug 18, 2026 |
| Optimize bundle size for docs site | Engineering | Medium | Done | Jul 30, 2026 |
| Coordinate partner co-marketing post | Marketing | Low | Done | Aug 10, 2026 |
How it works
1.One normalized filter shape drives everything
ActiveFilter is a small discriminated union — dateRange or multiSelect, each tagged with the columnId it targets. It's the single source of truth for the toolbar controls, the chips row, and the table's columnFilters, so none of those three can drift out of sync with each other.
export type DateRangeFilter = {type: "dateRange";columnId: string;from?: Date;to?: Date;};export type MultiSelectFilter = {type: "multiSelect";columnId: string;values: string[];};export type ActiveFilter = DateRangeFilter | MultiSelectFilter;
2.columnFilters is derived, not owned by the table
The table never manages its own columnFilters state. It's computed from `filters` on every render, so TanStack Table just reads whatever the ActiveFilter[] array currently says — there's nowhere else column filter state could live.
const columnFilters = useMemo<ColumnFiltersState>(() =>filters.map((f) =>f.type === 'multiSelect'? { id: f.columnId, value: f.values }: { id: f.columnId, value: { from: f.from, to: f.to } },),[filters],)
3.Chips read the same array they remove from
ActiveFilterChips takes the exact ActiveFilter[] array and renders one chip per entry, formatting a dateRange differently from a multiSelect. Removing a chip just filters that columnId out of `filters` — the derived columnFilters and the toolbar controls update for free.
function formatFilterLabel(f: ActiveFilter) {if (f.type === "dateRange") {return `${f.columnId}: ${f.from ? format(f.from, "MMM d") : "?"} - ${f.to ? format(f.to, "MMM d") : "?"}`;}return `${f.columnId}: ${f.values.join(", ")}`;}