Migrate @tanstack/vue-table v8 → v9
Dependencies
pnpm add @tanstack/vue-table@latest
# Optional, for external atoms during/after migration:
pnpm add @tanstack/vue-store
Vue has no /legacy entrypoint. The useLegacyTable React shim is React-only — Vue
projects rewrite each table directly. The good news: most call sites are mechanical renames.
Setup — the v8 vs v9 shape side-by-side
v8 (before)
<script setup lang="ts">
import {
FlexRender,
createColumnHelper,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
useVueTable,
} from '@tanstack/vue-table'
import type { ColumnDef } from '@tanstack/vue-table'
const columnHelper = createColumnHelper<Person>()
const columns: ColumnDef<Person, any>[] = [
columnHelper.accessor('age', { header: 'Age', sortingFn: 'alphanumeric' }),
]
const table = useVueTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
</script>
<template>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</template>
v9 (after)
<script setup lang="ts">
import {
FlexRender,
createColumnHelper,
createPaginatedRowModel,
createSortedRowModel,
rowPaginationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
useTable,
} from '@tanstack/vue-table'
const _features = tableFeatures({ rowSortingFeature, rowPaginationFeature })
const columnHelper = createColumnHelper<typeof _features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('age', { header: 'Age', sortFn: 'alphanumeric' }), // sortingFn → sortFn
])
const table = useTable({
_features,
_rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
paginatedRowModel: createPaginatedRowModel(),
},
columns,
data,
})
</script>
<template>
<!-- Preferred shorthand. Legacy :render / :props still compiles. -->
<FlexRender :cell="cell" />
</template>
Rename Cheat-Sheet
| v8 | v9 |
|---|---|
useVueTable(opts) |
useTable(opts, selector?) |
getCoreRowModel: getCoreRowModel() |
implicit; not an option |
getSortedRowModel: getSortedRowModel() |
_rowModels: { sortedRowModel: createSortedRowModel(sortFns) } |
getFilteredRowModel: getFilteredRowModel() |
_rowModels: { filteredRowModel: createFilteredRowModel(filterFns) } |
getPaginationRowModel: getPaginationRowModel() |
_rowModels: { paginatedRowModel: createPaginatedRowModel() } |
getGroupedRowModel: getGroupedRowModel() |
_rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) } |
createColumnHelper<TData>() |
createColumnHelper<typeof _features, TData>() |
ColumnDef<TData, TValue> |
ColumnDef<TFeatures, TData, TValue> |
Column<TData, TValue> / Row<TData> / Cell<TData, TValue> |
…<TFeatures, TData, TValue> |
sortingFn (column def) |
sortFn |
sortingFns |
sortFns |
getSortingFn() / getAutoSortingFn() |
getSortFn() / getAutoSortFn() |
SortingFn / SortingFns types |
SortFn / SortFns |
enablePinning: true |
enableColumnPinning and/or enableRowPinning |
state.columnSizingInfo |
state.columnResizing |
onColumnSizingInfoChange |
onColumnResizingChange |
ColumnSizing feature |
columnSizingFeature + columnResizingFeature (split) |
table.getState() |
table.store.state (full) / table.state (selector) / table.atoms.<slice>.get() |
row._getAllCellsByColumnId() |
row.getAllCellsByColumnId() (underscore removed) |
table._getFacetedRowModel() / _getFacetedMinMaxValues() / _getFacetedUniqueValues() |
Same names without leading underscore |
<FlexRender :render="…" :props="…" /> |
<FlexRender :cell="cell" /> / :header / :footer (preferred; legacy still works) |
Source: docs/framework/vue/guide/migrating.md, docs/framework/react/guide/migrating.md (the
non-Vue-specific renames are shared across adapters).
Core Patterns
1. Convert getXRowModel options to _rowModels factories
Every row-model factory now requires its *Fns parameter — this is what makes the registries
tree-shakeable in v9.
_rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
filteredRowModel: createFilteredRowModel(filterFns),
paginatedRowModel: createPaginatedRowModel(),
groupedRowModel: createGroupedRowModel(aggregationFns),
expandedRowModel: createExpandedRowModel(),
}
The *Fns registries are open-ended; do not cite a number of built-in fns.
2. Add tableFeatures and register every feature you use
If a feature isn't in _features, its API isn't on the table (TS error AND runtime
undefined). This is v9's biggest behavioral change.
import {
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
rowSelectionFeature,
tableFeatures,
} from '@tanstack/vue-table'
const _features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
columnFilteringFeature,
rowSelectionFeature,
})
For a quick migration that doesn't audit usage, stockFeatures is the "everything" escape
hatch — but it forfeits the bundle benefit. Prefer explicit registration.
3. Move state reads off table.getState()
// v8
const sorting = table.getState().sorting
// v9 — pick the narrowest read.
const sorting = table.atoms.sorting.get() // narrowest, no full state object built
const snapshot = table.store.state // full readonly view
const table = useTable(opts, (s) => ({ sorting: s.sorting })) // selected reactive state
table.state.sorting // typed selector output
For Vue reactivity, wrap an atom read in computed:
const sorting = computed(() => table.atoms.sorting.get())
4. v8 controlled state still works — just use getters
v8 controlled state via state + on[State]Change is preserved in v9 for migration paths.
The Vue-specific rule is that each state.<slice> must be a getter so Vue tracks .value.
const sorting = ref<SortingState>([])
const table = useTable({
_features,
_rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
columns,
data,
state: {
get sorting() {
return sorting.value
}, // ← getter, not raw ref
},
onSortingChange: (u) => {
sorting.value = typeof u === 'function' ? u(sorting.value) : u
},
})
For new code, prefer atoms: { sorting: sortingAtom } from @tanstack/vue-store — no
on[State]Change plumbing required. See tanstack-table/vue/compose-with-tanstack-store.
5. Update <FlexRender> shape
<!-- v8 (still works in v9, but verbose) -->
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
<!-- v9 preferred -->
<FlexRender :cell="cell" />
<FlexRender :header="header" />
<FlexRender :footer="header" />
Source: packages/vue-table/src/FlexRender.ts.
Common Mistakes
Importing useVueTable (CRITICAL)
// ❌
import { useVueTable } from '@tanstack/vue-table'
// ✅
import { useTable } from '@tanstack/vue-table'
useVueTable is not exported. Migration is a rename.
Trying to use useLegacyTable in Vue (CRITICAL)
useLegacyTable is a React-only shim from @tanstack/react-table/legacy. There is no Vue
equivalent. If you have many tables to convert, do them one file at a time — the renames are
mechanical.
Passing getCoreRowModel: getCoreRowModel() as an option (CRITICAL)
// ❌ v8 muscle memory.
const table = useTable({
_features,
columns,
data,
getCoreRowModel: getCoreRowModel(),
})
// ✅ Core row model is implicit. `_rowModels: {}` is fine.
const table = useTable({ _features, _rowModels: {}, columns, data })
Forgetting _features (CRITICAL)
_features is required even for a no-features migration. Pass tableFeatures({}) for empty,
or list everything you use. Without it: '_features' is missing in type.
Wrong createColumnHelper generic arity (CRITICAL)
// ❌ v8
const columnHelper = createColumnHelper<Person>()
// ✅ v9
const columnHelper = createColumnHelper<typeof _features, Person>()
Same applies to type annotations: ColumnDef<typeof _features, Person>,
Row<typeof _features, Person>, Cell<typeof _features, Person, unknown>.
Forgetting to pass sortFns / filterFns to row-model factories (CRITICAL)
// ❌ Runtime: no sort fns registered, sort is a no-op.
sortedRowModel: createSortedRowModel()
// ✅
sortedRowModel: createSortedRowModel(sortFns)
Missed sortingFn → sortFn rename (HIGH)
// ❌ v8 name on a column def. TypeScript will complain.
columnHelper.accessor('age', { header: 'Age', sortingFn: 'alphanumeric' })
// ✅
columnHelper.accessor('age', { header: 'Age', sortFn: 'alphanumeric' })
enablePinning: true (HIGH)
Split in v9 — pick one or both:
useTable({
// ...
enableColumnPinning: true,
enableRowPinning: true,
})
Reading table.getState() everywhere (HIGH)
The method was removed. Replace each call site explicitly — the right substitute depends on
the read pattern (see "Move state reads off table.getState()" above).
Underscore-prefixed APIs (HIGH)
row._getAllCellsByColumnId() // ❌
row.getAllCellsByColumnId() // ✅
table._getFacetedRowModel() // ❌
table.getFacetedRowModel() // ✅
Drop the underscore on every former internal API.
Bundling stockFeatures and calling it done (HIGH)
stockFeatures is the "ship everything" escape hatch — useful during the rename pass to keep
things compiling, but if you don't go back and replace it with an explicit tableFeatures({...})
that lists only what you actually render, you forfeit v9's tree-shaking — which is one of the
main reasons to migrate.
Hallucinating pre-v9 names (CRITICAL — top AI tell)
useTable from react-table v7, useVueTable from v8, getCoreRowModel() as an option,
single-generic createColumnHelper<TData>(), sortingFn, enablePinning, table.getState() —
all v7/v8 shapes. None of them compile in v9.
Reimplementing what built-in APIs already provide (CRITICAL — #1 AI tell)
Migration is a great time to delete hand-rolled sort/filter/select state machines and route
through table.setSorting, table.setColumnFilters, row.toggleSelected, table.nextPage,
etc.
See Also
tanstack-table/vue/getting-started— the v9 minimum-viable shapetanstack-table/vue/table-state— reactivity model + reading statetanstack-table/vue/production-readiness— finish the migration with bundle + identity auditstanstack-table/table-core/setup—_features/_rowModelsdeep divetanstack-table/table-core/column-definitions— column helper + generics
Source: TanStack/table — distributed by TomeVault.