@vates/data-table-solid
v0.13.0
Published
Solid.js adapter for data-table
Readme
@vates/data-table-solid
Solid.js adapter for data-table — a flexible, fully-typed data table with sorting, filtering, column visibility/reordering, and row grouping.
If you're not already using Solid, use @vates/data-table-vanilla instead — it wraps this package behind a plain createDataTable(container, options) API and bundles Solid internally, so it needs no framework installed.
Install
npm install @vates/data-table-solid solid-jssolid-js is a peer dependency (>=1.9.0) — install it alongside this package rather than letting it be bundled, so the table shares your app's own reactive runtime. Two separate copies of solid-js in one page don't just cost bytes: Solid's dependency tracking is scoped to whichever module instance created a signal, so a computation running under a different copy silently never sees updates to it.
Usage
import { DataTable, type ColumnDef } from '@vates/data-table-solid'
interface Employee {
id: number
name: string
department: string
salary: number
}
const COLUMNS: ColumnDef<Employee>[] = [
{ key: 'name', label: 'Name', type: 'string' },
{ key: 'department', label: 'Department', type: 'string', groupable: true },
{
key: 'salary',
label: 'Salary',
type: 'number',
format: (v) => Number(v).toLocaleString() + ' €',
},
]
export default function EmployeeTable(props: { employees: Employee[] }) {
return <DataTable data={props.employees} columns={COLUMNS} rowKey="id" />
}data/columns are tracked reactively — props.employees changing later is all it takes, no createEffect needed. CSS is injected automatically into <head> the first time the table mounts — there's nothing extra to import. This includes all color tokens and dark-mode overrides that activate automatically via prefers-color-scheme: dark; see the theming section of the vanilla README for the full token table and how to override it (the CSS itself is shared between both packages).
createTableState/DataTableView, and reaching state <DataTable> can't expose
<DataTable> covers the common case, but it never hands back the underlying TableState — so it can't be used for view persistence, an imperative selection API, or anything else that needs to act on the table from outside. For that, build the two pieces <DataTable> itself is made of directly:
createTableState(data, columns, options?)mirrorspackages/react/packages/vue's ownuseTableStatein internal state/action logic, and returns aTableState<TRow>— but namespaced by concern (table.sort.*,table.filter.*,table.group.*,table.selection.*,table.pagination.*,table.search.*,table.columns.*, plus a handful of top-level entries likeprocessedData/pagedData/getViewState) rather than the flat ~45-field shape react/vue still return — see CLAUDE.md's "Namespaced TableState" for the full design. Unlike those two (which get freshdata/columnsarguments on every re-invocation), this one ownsdata/columnsas its own signals, withsetData/columns.setsetters to update them.data/columnseach accept a plain array (a one-time initial value, exactly likepackages/vanilla's owncreateDataTable) or a SolidAccessor— pass one and it's tracked reactively for the table's whole lifetime, with nocreateEffectto write yourself (this is what<DataTable>itself is built on).The 3rd
optionsargument accepts either a plainCreateTableStateOptionsobject or anAccessorreturning one — unlikedata/columns, individual option fields can't each independently be "value or Accessor" (getRowIdis itself a function, indistinguishable at runtime from an Accessor returning one), so reactivity is lifted to the whole options object instead. Passing an Accessor keepslabels/defaultGroupsCollapsed/getRowIdlive (a later change takes effect immediately, no need to recreate the table);initialViewState— construction-time defaults for columns/sort/filters/grouping/page/search, also whatresetViewrestores — stays seed-only either way.Two fields on
TableStateexist only here, not on React/Vue's:table.columns.list/table.columns.set(the raw column signal/setter — React/Vue never had this onTableStateat all, since they get freshcolumnsas a constructor argument instead) andtable.selection.setAll(replaces the selection outright by reference; mainly exists to back@vates/data-table-vanilla's imperativesetSelection(rows)).table.labelsis also the one field that differs in kind from React/Vue: it's acreateMemo, called astable.labels(), not a plain object — it has to react to a changedlabelsoption itself.
const table = createTableState(data, columns)
table.sort.toggle('score') // was table.toggleSort('score')
table.filter.cycleValue('dept', 'Eng') // was table.cycleFilterValue('dept', 'Eng')
table.selection.toggle(row) // was table.toggleRowSelection(row)
table.pagination.setPage(2) // was table.setPage(2)<DataTableView table={...} data={...} columns={...} .../>is the render layer, taking thatTableStateas a prop rather than building one itself — the same split React/Vue use for their ownDataTableView.
const table = createTableState(
() => props.data,
() => props.columns,
)
return <DataTableView table={table} />DataTableViewProps only takes table (plus rowKey/selectable/onRowClick) — no separate data/columns props, since table.data()/table.columns.list() already are that value.
View persistence & sharing
getViewState()/setViewState() capture and apply a serializable snapshot of sort, filters, groups, page, etc. — everything except selection, which is identity-based and not meaningful to persist or share. Opt-in helpers wire this up to localStorage and the URL, matching React/Vue's own:
import { createTableState, usePersistence } from '@vates/data-table-solid'
const table = createTableState(data, columns)
const { reset } = usePersistence(table, { storageKey: 'my-table-view', paramName: 'view' })usePersistence combines usePersistedView (loads on mount, saves on every change) and useUrlView (loads from ?view=... on mount and on back/forward navigation, writes back via history.replaceState) behind one options object, so storageKey/paramName are written down once instead of separately at each call site. Its returned reset() puts the table back to its construction-time defaults and clears whatever was persisted — equivalent to calling resetView(table, { storageKey: 'my-table-view', paramName: 'view' }) yourself. Use usePersistedView/useUrlView/resetView directly instead if you only want one of the two (e.g. URL sharing with no localStorage).
To persist a view somewhere else (e.g. a backend), call getViewState()/setViewState(view) directly — these helpers work with any object shaped like { getViewState(), setViewState(view) }.
<DataTable> builds its own createTableState internally, so these helpers can't reach it — see the createTableState/DataTableView section above for the split that lets you own the state yourself.
Selection, row click, keyboard navigation, view persistence
Same model as every other adapter — see the root README and CLAUDE.md for the full behavior (selection is tracked by object identity, not rowKey; shift-click/shift-arrow range selection; roving-tabindex keyboard nav; getViewState()/setViewState() for persistence/sharing). TableState<TRow> exposes the same actions/derived values React's and Vue's useTableState do, namespaced by concern (see createTableState's own entry above) — table.selection.all/.rows/.toggle/.toggleAll/.clear, table.sort.entries, table.filter.include, table.group.by, table.pagination.page/.pageSize, and so on, all as Solid signals/accessors instead of useState/ref. <DataTable> doesn't expose any of that directly (see above) — pass selectable to turn selection on, and onSelectionChange to observe it, the same two props @vates/data-table-vanilla's own createDataTable accepts.
Object-identity selection silently drops on a setData/refetch that produces new row objects, since a Set can only ever match by reference. Pass getRowId (to createTableState's options, or <DataTable>'s own prop) to opt into id-based matching instead — a selected id is remapped to its fresh object reference whenever data changes, and dropped if the id no longer exists:
const table = createTableState(data, columns, { getRowId: (row) => row.id })Known limitations
Full keyboard-nav and virtualization parity with React/Vue: the flat filter checklist is virtualized, roving Up/Down/Home/End nav inside an open dropdown, dropdown focus-on-open, Sort/Group's activate/remove focus retention, the Filter dropdown's Left/Right pane-crossing nav, and TableBody's cross-page Home/End/Arrow nav are all implemented. See docs/solid-package.md for the full detail.
License
MIT
