TanStack Table + DataView
The only sanctioned way to render a list. Never hand-roll a <table>.
@tanstack/react-table (^8.21.3) is the dashboard's table engine. It sits behind a
small set of in-repo primitives under src/components/table/, exposed as a DataView
component that toggles between grid, list, and table views with a shared
toolbar (search, filter, export).
Never hand-roll a <table>. If you find yourself writing <thead>, <tbody>,
manual sort handlers, or a "just for this one page" filter input — stop, and use
DataView. Every list surface in the dashboard runs through this primitive.
Why TanStack (and not a UI table component)
We need:
- Three render modes from one source of truth (grid card, list row, table row).
- Column virtualization for large feed/instance counts.
- Headless — the dashboard's row, header, and toolbar visuals are bespoke; we don't want a styled table fighting our Tailwind tokens.
TanStack is headless and supports all three concerns. The thin DataView wrapper
provides the dashboard-flavored chrome.
Where it lives in src/
| File | Role |
|---|---|
src/components/table/DataView.tsx | The top-level component — takes columns + rows + view mode, dispatches to grid/list/table renderers |
src/components/table/ (siblings) | Toolbar, filter chips, search input, export buttons, view toggle |
src/components/FilterBar.tsx | The reusable filter-chip bar used by every plane |
src/components/ViewToggle.tsx | Grid / list / table radio toggle |
src/hooks/useViewMode.ts | localStorage-backed view-mode preference per route |
src/lib/downloadCsv.ts, src/lib/downloadJson.ts | Export helpers wired into the toolbar |
Existing consumers
| Page | Component | Mode default |
|---|---|---|
| Fleet → Instances | InstanceTable / InstanceGridView / InstanceList | Table |
| Fleet → Instance Management | InstancesManagementTable | Table |
| Fleet → Flows | FlowTable / FlowGridView | Grid |
| Restreaming → HoloVids (feed picker) | HoloVidsAssignmentEditor (uses DataView for source feeds) | Grid |
| Republic | Republic tables (member nations, holdings) | Table |
| Temple Archives | Per-archive lists | Table |
Column definition pattern
Every consumer defines columns the same way:
const columns: ColumnDef<Row>[] = [
{ accessorKey: 'id', header: 'ID', cell: ({ row }) => row.original.id },
{ accessorKey: 'status', header: 'Status', cell: StatusPillCell },
// ...
];The cell renderer is shared across all three view modes — the grid card and the
table row pull from the same React node tree, just laid out differently by the parent.
Toolbar contract
A DataView toolbar always includes:
- Search input (filters across all string columns by default).
- Filter chips (driven by
FilterBar). - View toggle (grid / list / table).
- Export buttons (CSV / JSON via
downloadCsv/downloadJson).
Pages that need a "create" or "bulk" button add it to the left of the toolbar via
the toolbarLeft prop. Pages that need a per-row action add it as a column with a
custom cell renderer.
Memory rule
The constraint is stricter than "prefer this primitive" — it's a project rule:
Never hand-roll a
<table>; use DataView/TanStack fromsrc/components/table/.
Hand-rolled tables drift on sort behavior, keyboard nav, export semantics, and the view-toggle interaction. They've been caught and replaced; don't reintroduce them.
Extension points
- New column type — write a cell renderer component and import it; don't extend the column-def shape.
- New view mode — extend
DataViewitself. Don't fork. - New export format — add a helper to
src/lib/and a button to the toolbar.
See also
- Components — every plane that lists things
- Tailwind v4 — table chrome tokens