.agents/skills/tanstack-table/SKILL.md
Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit.
npx skillsauth add em-jones/staccato-toolkit tanstack-tableInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
TanStack Table is a headless UI library for building data tables and datagrids. It provides logic for sorting, filtering, pagination, grouping, expanding, column pinning/ordering/visibility/resizing, and row selection - without rendering any markup or styles.
Package: @tanstack/solid-table
Utilities: @tanstack/match-sorter-utils (fuzzy filtering)
Current Version: v8
npm install @tanstack/solid-table
// WRONG - new references every render, causes infinite loops
const table = createSolidTable({
data: fetchedData.results, // new ref!
columns: [{ accessorKey: 'name' }], // new ref!
})
// CORRECT - stable references (Solid uses createMemo)
const columns = createMemo(() => [...])
const data = createMemo(() => fetchedData?.results ?? [])
const table = createSolidTable({
get data() { return data() },
get columns() { return columns() },
getCoreRowModel: getCoreRowModel(),
})
import { createColumnHelper } from '@tanstack/solid-table'
type Person = {
firstName: string
lastName: string
age: number
status: 'active' | 'inactive'
}
const columnHelper = createColumnHelper<Person>()
const columns = [
// Accessor column (data column)
columnHelper.accessor('firstName', {
header: 'First Name',
cell: info => info.getValue(),
footer: info => info.column.id,
}),
// Accessor with function
columnHelper.accessor(row => row.lastName, {
id: 'lastName', // required with accessorFn
header: () => <span>Last Name</span>,
cell: info => <i>{info.getValue()}</i>,
}),
// Display column (no data, custom rendering)
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<button onClick={() => deleteRow(row.original)}>Delete</button>
),
}),
// Group column (nested headers)
columnHelper.group({
id: 'info',
header: 'Info',
columns: [
columnHelper.accessor('age', { header: 'Age' }),
columnHelper.accessor('status', { header: 'Status' }),
],
}),
]
| Option | Type | Description |
|--------|------|-------------|
| id | string | Unique identifier (auto-derived from accessorKey) |
| accessorKey | string | Dot-notation path to row data |
| accessorFn | (row) => any | Custom accessor function |
| header | string \| (context) => JSX.Element | Header renderer |
| cell | (context) => JSX.Element | Cell renderer |
| footer | (context) => JSX.Element | Footer renderer |
| size | number | Default width (default: 150) |
| minSize | number | Min width (default: 20) |
| maxSize | number | Max width |
| enableSorting | boolean | Enable sorting |
| sortingFn | string \| SortingFn | Sort function |
| enableFiltering | boolean | Enable filtering |
| filterFn | string \| FilterFn | Filter function |
| enableGrouping | boolean | Enable grouping |
| aggregationFn | string \| AggregationFn | Aggregation function |
| enableHiding | boolean | Enable visibility toggle |
| enableResizing | boolean | Enable resizing |
| enablePinning | boolean | Enable pinning |
| meta | any | Custom metadata |
import { createSignal } from 'solid-js'
import {
createSolidTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
} from '@tanstack/solid-table'
import type { SortingState, ColumnFiltersState, PaginationState } from '@tanstack/table-core'
function MyTable(props: { data: Person[]; columns: ColumnDef<Person, unknown>[] }) {
const [sorting, setSorting] = createSignal<SortingState>([])
const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([])
const [pagination, setPagination] = createSignal<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = createSolidTable({
get data() { return props.data },
get columns() { return props.columns },
state: {
get sorting() { return sorting() },
get columnFilters() { return columnFilters() },
get pagination() { return pagination() },
},
onSortingChange: (updater) => setSorting(updater),
onColumnFiltersChange: (updater) => setColumnFilters(updater),
onPaginationChange: (updater) => setPagination(updater),
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return (
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: ' ↑',
desc: ' ↓',
}[header.column.getIsSorted() as string] ?? null}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
In Solid, pass signals using getter functions for reactivity:
const [data, setData] = createSignal<Person[]>([])
const [columns] = createMemo(() => [...])
const table = createSolidTable({
get data() { return data() },
get columns() { return columns() },
// ...
})
Use initialState to set default values without controlling:
const table = createSolidTable({
data,
columns,
initialState: {
columnOrder: ['age', 'firstName', 'lastName'],
columnVisibility: { id: false },
expanded: true,
sorting: [{ id: 'age', desc: true }],
pagination: { pageIndex: 0, pageSize: 10 },
},
})
For complete control, use onStateChange:
const table = createSolidTable({
get data() { return data() },
get columns() { return columns() },
})
const [state, setState] = createSignal({
...table.initialState,
pagination: { pageIndex: 0, pageSize: 15 },
})
table.setOptions(prev => ({
...prev,
get state() { return state() },
onStateChange: setState,
}))
const [sorting, setSorting] = createSignal<SortingState>([])
const table = createSolidTable({
state: {
get sorting() { return sorting() },
},
onSortingChange: (updater) => setSorting(updater),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
enableMultiSort: true,
// manualSorting: true, // For server-side sorting
})
// Built-in sort functions: 'alphanumeric', 'text', 'datetime', 'basic'
// Column-level: sortingFn: 'alphanumeric'
const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([])
const table = createSolidTable({
state: {
get columnFilters() { return columnFilters() },
},
onColumnFiltersChange: (updater) => setColumnFilters(updater),
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
})
// Built-in: 'includesString', 'equalsString', 'arrIncludes', 'inNumberRange', etc.
// Filter UI
function Filter(props: { column: Column<any, unknown> }) {
return (
<input
value={(props.column.getFilterValue() ?? '') as string}
onInput={e => props.column.setFilterValue(e.target.value)}
placeholder={`Filter... (${props.column.getFacetedUniqueValues()?.size})`}
/>
)
}
const [globalFilter, setGlobalFilter] = createSignal('')
const table = createSolidTable({
state: {
get globalFilter() { return globalFilter() },
},
onGlobalFilterChange: (updater) => setGlobalFilter(updater),
globalFilterFn: 'includesString',
getFilteredRowModel: getFilteredRowModel(),
})
import { rankItem } from '@tanstack/match-sorter-utils'
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value)
addMeta({ itemRank })
return itemRank.passed
}
const table = createSolidTable({
filterFns: { fuzzy: fuzzyFilter },
globalFilterFn: 'fuzzy',
})
const [pagination, setPagination] = createSignal<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = createSolidTable({
state: {
get pagination() { return pagination() },
},
onPaginationChange: (updater) => setPagination(updater),
getPaginationRowModel: getPaginationRowModel(),
// For server-side:
// manualPagination: true,
// pageCount: serverPageCount,
})
// Navigation
table.nextPage()
table.previousPage()
table.firstPage()
table.lastPage()
table.setPageSize(20)
table.getCanNextPage() // boolean
table.getCanPreviousPage() // boolean
table.getPageCount() // total pages
const [rowSelection, setRowSelection] = createSignal<RowSelectionState>({})
const table = createSolidTable({
state: {
get rowSelection() { return rowSelection() },
},
onRowSelectionChange: (updater) => setRowSelection(updater),
enableRowSelection: true,
enableMultiRowSelection: true,
})
// Checkbox column
columnHelper.display({
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
/>
),
})
// Get selected rows
table.getSelectedRowModel().rows
const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>({})
const table = createSolidTable({
state: {
get columnVisibility() { return columnVisibility() },
},
onColumnVisibilityChange: (updater) => setColumnVisibility(updater),
})
// Toggle UI
<table>
{table.getAllLeafColumns().map(column => (
<label>
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/>
{column.id}
</label>
))}
</table>
const [columnPinning, setColumnPinning] = createSignal<ColumnPinningState>({
left: ['select', 'name'],
right: ['actions'],
})
const table = createSolidTable({
state: {
get columnPinning() { return columnPinning() },
},
onColumnPinningChange: (updater) => setColumnPinning(updater),
enableColumnPinning: true,
})
// Render pinned sections separately
row.getLeftVisibleCells() // Left-pinned
row.getCenterVisibleCells() // Unpinned
row.getRightVisibleCells() // Right-pinned
const table = createSolidTable({
enableColumnResizing: true,
columnResizeMode: 'onChange', // 'onChange' | 'onEnd'
defaultColumn: { size: 150, minSize: 50, maxSize: 500 },
})
// Resize handle in header
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
classList={{ resizer: true, isResizing: header.column.getIsResizing() }}
/>
const [grouping, setGrouping] = createSignal<GroupingState>([])
const table = createSolidTable({
state: {
get grouping() { return grouping() },
},
onGroupingChange: (updater) => setGrouping(updater),
getGroupedRowModel: getGroupedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
})
// Built-in aggregation: 'sum', 'min', 'max', 'mean', 'median', 'count', 'unique', 'uniqueCount'
columnHelper.accessor('amount', {
aggregationFn: 'sum',
aggregatedCell: ({ getValue }) => `Total: ${getValue()}`,
})
const [expanded, setExpanded] = createSignal<ExpandedState>({})
const table = createSolidTable({
state: {
get expanded() { return expanded() },
},
onExpandedChange: (updater) => setExpanded(updater),
getExpandedRowModel: getExpandedRowModel(),
getSubRows: (row) => row.subRows, // For hierarchical data
})
// Expand toggle
<button onClick={row.getToggleExpandedHandler()}>
{row.getIsExpanded() ? '−' : '+'}
</button>
// Detail row pattern
{row.getIsExpanded() && (
<tr>
<td colSpan={columns.length}>
<DetailComponent data={row.original} />
</td>
</tr>
)}
import { createVirtualizer } from '@tanstack/solid-virtual'
function VirtualizedTable() {
const table = createSolidTable({ /* ... */ })
const { rows } = table.getRowModel()
let parentRef: HTMLDivElement | undefined
const virtualizer = createVirtualizer({
count: rows.length,
getScrollElement: () => parentRef ?? null,
estimateSize: () => 35,
overscan: 10,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<table>
<tbody style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => {
const row = rows[virtualRow.index]
return (
<tr
key={row.id}
style={{
position: 'absolute',
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
const table = createSolidTable({
get data() { return serverData },
get columns() { return columns },
manualSorting: true,
manualFiltering: true,
manualPagination: true,
pageCount: serverPageCount,
state: {
get sorting() { return sorting() },
get columnFilters() { return columnFilters() },
get pagination() { return pagination() },
},
onSortingChange: (updater) => setSorting(updater),
onColumnFiltersChange: (updater) => setColumnFilters(updater),
onPaginationChange: (updater) => setPagination(updater),
getCoreRowModel: getCoreRowModel(),
// Do NOT include getSortedRowModel, getFilteredRowModel, getPaginationRowModel
})
// Fetch data based on state (using createEffect)
createEffect(() => {
fetchData({ sorting: sorting(), filters: columnFilters(), pagination: pagination() })
})
declare module '@tanstack/solid-table' {
interface ColumnMeta<TData extends RowData, TValue> {
filterVariant?: 'text' | 'range' | 'select'
align?: 'left' | 'center' | 'right'
}
}
declare module '@tanstack/solid-table' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface SortingFns {
myCustomSort: SortingFn<unknown>
}
}
declare module '@tanstack/solid-table' {
interface TableMeta<TData extends RowData> {
updateData: (rowIndex: number, columnId: string, value: unknown) => void
}
}
const table = createSolidTable({
meta: {
updateData: (rowIndex, columnId, value) => {
setData(old => old.map((row, i) =>
i === rowIndex ? { ...row, [columnId]: value } : row
))
},
},
})
import { createSignal, createMemo, createEffect } from 'solid'
import {
createColumnHelper,
flexRender,
createSolidTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getGroupedRowModel,
getExpandedRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFacetedMinMaxValues,
} from '@tanstack/solid-table'
import type {
ColumnDef,
SortingState,
ColumnFiltersState,
VisibilityState,
PaginationState,
ExpandedState,
RowSelectionState,
GroupingState,
ColumnOrderState,
ColumnPinningState,
FilterFn,
SortingFn,
} from '@tanstack/table-core'
createMemo for data and columns to prevent infinite re-rendersflexRender for all header/cell/footer renderingtable.getRowModel().rows for final rendered rows (not getCoreRowModel)getRowId for stable row keys when data has unique IDsmanualX options for server-side operationsstate.X (as getter) and onXChangeautoResetPageIndex: true when filtering should reset paginationget sorting() { return sorting() } for reactivitygetCoreRowModel() (required for all tables)id when using accessorFnmanualPagination with client-side getPaginationRowModelcolSpan for grouped headersheader.isPlaceholder for group column spacersstate: { sorting } should be state: { get sorting() { return sorting() } }tools
<!--VITE PLUS START--> # Using Vite+, the Unified Toolchain for the Web This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, but it invokes Vite through `vp dev` and `vp build`. ## Vite+ Workflow `vp` is a global binary that handles the full development lifecycle. Run `vp help` to pr
development
Guide for building performant data tables. Uses tanstack-table for table logic (sorting, filtering, pagination) and tanstack-virtual for rendering large datasets efficiently.
development
Expert guidance for building observable, expressive, and fault-tolerant TypeScript applications using the effect-ts/effect ecosystem. Covers Effect<A, E, R> type, error management, dependency injection via Layers, observability (logging, metrics, tracing), concurrency with Fibers, retry/scheduling, Schema validation, Streams, and Sinks.
tools
Complete E2E (end-to-end) and integration testing skill for TypeScript/NestJS projects using Jest, real infrastructure via Docker, and GWT pattern. ALWAYS use this skill when user needs to: **SETUP** - Initialize or configure E2E testing infrastructure: - Set up E2E testing for a new project - Configure docker-compose for testing (Kafka, PostgreSQL, MongoDB, Redis) - Create jest-e2e.config.ts or E2E Jest configuration - Set up test helpers for database, Kafka, or Redis - Configure .env.e2e environment variables - Create test/e2e directory structure **WRITE** - Create or add E2E/integration tests: - Write, create, add, or generate e2e tests or integration tests - Test API endpoints, workflows, or complete features end-to-end - Test with real databases, message brokers, or external services - Test Kafka consumers/producers, event-driven workflows - Working on any file ending in .e2e-spec.ts or in test/e2e/ directory - Use GWT (Given-When-Then) pattern for tests **REVIEW** - Audit or evaluate E2E tests: - Review existing E2E tests for quality - Check test isolation and cleanup patterns - Audit GWT pattern compliance - Evaluate assertion quality and specificity - Check for anti-patterns (multiple WHEN actions, conditional assertions) **RUN** - Execute or analyze E2E test results: - Run E2E tests - Start/stop Docker infrastructure for testing - Analyze E2E test results - Verify Docker services are healthy - Interpret test output and failures **DEBUG** - Fix failing or flaky E2E tests: - Fix failing E2E tests - Debug flaky tests or test isolation issues - Troubleshoot connection errors (database, Kafka, Redis) - Fix timeout issues or async operation failures - Diagnose race conditions or state leakage - Debug Kafka message consumption issues **OPTIMIZE** - Improve E2E test performance: - Speed up slow E2E tests - Optimize Docker infrastructure startup - Replace fixed waits with smart polling - Reduce beforeEach cleanup time - Improve test parallelization where safe Keywords: e2e, end-to-end, integration test, e2e-spec.ts, test/e2e, Jest, supertest, NestJS, Kafka, Redpanda, PostgreSQL, MongoDB, Redis, docker-compose, GWT pattern, Given-When-Then, real infrastructure, test isolation, flaky test, MSW, nock, waitForMessages, fix e2e, debug e2e, run e2e, review e2e, optimize e2e, setup e2e