@tanstack/vue-table 9.2.4
Headless table adapter for Vue 3. Re-exports all of @tanstack/table-core 9.2.4 plus Vue bindings: useTable, FlexRender, flexRender, createTableHook (dist/index.d.ts:4-5).
Environment limits: vue >=3.2 peer dependency, Node >=20, runtime dep @tanstack/store ^0.11.1 (package.json:34-53).
This Skill targets v9 only. Code using useVueTable, getCoreRowModel(), or onStateChange is v8; migrate it first with Migration v8 to v9.
Quick start
<script setup lang="ts">
import { ref } from 'vue'
import { FlexRender, tableFeatures, useTable } from '@tanstack/vue-table'
type Person = { name: string; age: number }
const features = tableFeatures({})
const columns = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'age', header: 'Age' },
]
const data = ref<Person[]>([{ name: 'Ada', age: 36 }])
const table = useTable({ features, columns, data })
</script>
<template>
<table>
<thead>
<tr v-for="group in table.getHeaderGroups()" :key="group.id">
<th v-for="header in group.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :header="header" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in table.getRowModel().rows" :key="row.id">
<td v-for="cell in row.getAllCells()" :key="cell.id">
<FlexRender :cell="cell" />
</td>
</tr>
</tbody>
</table>
</template>
Source: skills/getting-started/SKILL.md (shipped in the package).
Add features explicitly
Every feature beyond core must be registered, and each row-model slot follows its prerequisite feature in the same tableFeatures call. The core row model is automatic; there is no getCoreRowModel() option.
import {
createSortedRowModel,
rowSortingFeature,
sortFn_alphanumeric,
tableFeatures,
} from '@tanstack/vue-table'
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns: { alphanumeric: sortFn_alphanumeric },
})
APIs are feature-gated: a missing table.atoms.pagination means rowPaginationFeature was not registered. Full feature and slot list: API surface.
State in one place per slice
State is atom-based. Read it inside a tracked boundary (template, computed, watch, or table.Subscribe); a bare read is only a snapshot.
const pageIndex = computed(() => table.atoms.pagination.get().pageIndex)
Controlled slices need a reactive state plus the matching on[State]Change callback that resolves value-or-function updaters:
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 20 })
const state = computed(() => ({ pagination: pagination.value }))
const onPaginationChange = (next: PaginationState | ((old: PaginationState) => PaginationState)) => {
pagination.value = typeof next === 'function' ? next(pagination.value) : next
}
// pass state and onPaginationChange to useTable
Own each slice exactly once: internal default, initialState, external atoms, or state + callback. Never both atoms.pagination and state.pagination. onStateChange no longer exists; observe table.store instead. Details: API surface.
Rules that prevent most bugs
- Pass
dataas aref/computed, neverdata.value(dist/useTable.d.ts:13-19). Update it by replacing the array (.value = [...]); in-placepush/spliceis not tracked because writable atoms useshallowRef(dist/reactivity.js:50-52). - Keep
featuresandcolumnsstable; do not rebuild arrays per render. - Render through
FlexRendershorthand::cell,:header,:footer. The legacy:render/:propsform still compiles but is the migration target, not the default (dist/FlexRender.d.ts:28-41). - Never destructure or spread row/cell/column/header methods; they are prototype methods using
this. Userow.getValue('name'), notconst read = row.getValue. - In JSX, pass
table.Subscribeits render function as the explicitchildrenprop; Vue JSX children become slots (dist/useTable.d.ts:9-11). - For server-driven tables set the
manual*option (manualPagination: true) and passrowCount. - Optional
stockFeaturesbundles everything; use it only as a temporary audit bridge, not production.
Common mistakes
- HIGH: renaming
useVueTabletouseTablewhile keepinggetSortedRowModeloptions. Move row models and registries intotableFeatures(skills/migrate-v8-to-v9/SKILL.md:156-159). - HIGH:
useTable({ data: data.value })freezes one snapshot; passdata(skills/getting-started/SKILL.md:85-99). - HIGH: reading
table.atoms.sorting.get()outside a tracked boundary; wrap incomputed(skills/table-state/SKILL.md:104-116). - HIGH: assigning the updater function itself (
pagination.value = next) instead of resolving it first (skills/table-state/SKILL.md:146-161). - HIGH:
columnPinning.left/rightno longer exist; v9 usesstart/endlogical pinning (skills/migrate-v8-to-v9/SKILL.md:99-120). - HIGH:
createColumnHelper<Person>()now needs features first:createColumnHelper<typeof features, Person>()(skills/migrate-v8-to-v9/SKILL.md:147-148).
Reusable app conventions
For app-wide table defaults, use createTableHook({ features, tableComponents, cellComponents, headerComponents }) to get useAppTable, createAppColumnHelper, and typed context hooks (dist/createTableHook.d.ts:136-153). Export the context hook with an explicit type to avoid circular inference when registered components import the hook module (skills/create-table-hook/SKILL.md:62-84).
Integrations
- TanStack Query: manual pagination with reactive keys,
rowCount, computed data passthrough. Integrations - TanStack Virtual: virtualize
table.getRowModel().rows, never the source array. Integrations
References
- API surface: exports, subpaths,
useTable/Subscribetypes, features and slots, atoms model. - Migration v8 to v9: full breaking-change map, pinning renames, TypeScript changes, checklist.
- Integrations: TanStack Query and TanStack Virtual composition patterns.
Also in the package
The package ships its own agent skills under skills/ (getting-started, table-state, migrate-v8-to-v9, create-table-hook, with-tanstack-query, with-tanstack-virtual) and mentions npx @tanstack/intent@latest install to wire them into coding agents (README.md:70-78). Verify exact APIs in the installed node_modules/@tanstack/vue-table/dist/index.d.ts; do not reconstruct v9 from v8 memory.