name: vue/table-state
description: >
Vue reactivity for @tanstack/vue-table v9. Covers useTable(options, selector?), reactive
data/columns via ref/computed getters, the vueReactivity() binding (readonly atoms →
computed, writable → shallowRef, subscriptions via watch({flush:'sync'})), the three state
surfaces table.atoms.<slice> / table.store / table.state, selected state via the second
useTable argument, the <FlexRender> component, table.Subscribe for atom/source subscriptions,
and composition with createAtom / useSelector from @tanstack/vue-store.
type: framework
library: tanstack-table
framework: vue
library_version: '9.0.0-alpha.48'
requires:
- state-management
- setup
sources:
- docs/framework/vue/guide/table-state.md
- docs/framework/vue/vue-table.md
- packages/vue-table/src/useTable.ts
- packages/vue-table/src/reactivity.ts
- packages/vue-table/src/FlexRender.ts
- examples/vue/basic-use-table/
- examples/vue/basic-external-atoms/
- examples/vue/basic-external-state/
Vue Table State, Subscribe & createTableHook
Dependencies
pnpm add @tanstack/vue-table @tanstack/vue-store
@tanstack/vue-store is a peer for createAtom / useSelector. It is only required when you
opt into external atoms — basic tables that use initialState or built-in state work without it.
This skill is v9-specific (9.0.0-alpha.48). The hook is useTable for every framework now; the
v8 name useVueTable no longer exists.
Setup
Every Vue table call requires features (built from tableFeatures({...})). Row model
factories now live on the features object alongside the feature itself. Core row model is
automatic; only register paginatedRowModel, sortedRowModel, etc. when you use the matching
feature.
<script setup lang="ts">
import { ref } from 'vue'
import {
FlexRender,
createColumnHelper,
createSortedRowModel,
rowSortingFeature,
sortFns,
tableFeatures,
useTable,
} from '@tanstack/vue-table'
type Person = { firstName: string; lastName: string; age: number }
// Stable identity — declare outside the component or at module scope.
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
})
const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('firstName', { header: 'First' }),
columnHelper.accessor('lastName', { header: 'Last' }),
columnHelper.accessor('age', { header: 'Age' }),
])
const data = ref<Person[]>([])
const table = useTable({
features,
columns,
// Reactive data: pass the ref directly OR a getter — the adapter unwraps.
data,
})
</script>
<template>
<table>
<thead>
<tr v-for="hg in table.getHeaderGroups()" :key="hg.id">
<th v-for="h in hg.headers" :key="h.id">
<FlexRender v-if="!h.isPlaceholder" :header="h" />
</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>
Why this works
The Vue adapter calls vueReactivity() and installs it as coreReactivityFeature automatically
(see packages/vue-table/src/useTable.ts):
- Readonly atoms back onto
computed()refs. - Writable atoms back onto
shallowRef(). - Subscriptions use
watch(source, cb, { flush: 'sync' }), so table updates are visible to Vue render and computed work immediately.
useTable also runs a watch(() => getReactiveOptionDeps(...)) on every option, so passing a
ref or computed for data, columns, rowCount, etc. is supported — the table calls
setOptions whenever any reactive option changes.
Vue note.
table.Subscribeexists for parity with React, but you usually do not need it. Vue's reactivity re-evaluates template reads automatically — wrap reads incomputed(...)if you need them outside a template. Do not import React-Compiler workarounds.
Core Patterns
1. The three read surfaces
// (a) Per-slice atom — narrowest, no full state snapshot built
const sorting = table.atoms.sorting.get()
// (b) Flat readonly store — every registered slice as one object
const snapshot = table.state
// (c) Vue selected state — the value returned from useTable's 2nd arg
const table = useTable(
{
features,
columns,
data,
},
(state) => ({ sorting: state.sorting }),
)
table.state.sorting // typed, reactive
table.atoms.<slice> only contains slices for features registered in features. If
rowSortingFeature is not registered, table.atoms.sorting is undefined (and TypeScript
flags it). This is the v9-specific "missing API" gotcha — register the feature first.
Source: docs/framework/vue/guide/table-state.md (Feature-based State, Accessing Table State).
2. Reactive data with a getter or computed
The adapter accepts a ref/computed for any option. The idiomatic shapes are:
// (a) Pass the ref directly — adapter unwraps via `unref()`
const data = ref(makeData(100))
const table = useTable({ features, columns, data })
// (b) Use a getter when `data` is owned by a parent object
const table = useTable({
features,
columns,
get data() {
return data.value
},
})
// (c) Computed when filtering/derivation lives on the client
const filtered = computed(() => data.value.filter(/* … */))
const table = useTable({
features,
columns,
get data() {
return filtered.value
},
})
When data.value changes, useTable calls setOptions synchronously and the table re-derives.
Source: examples/vue/basic-use-table/src/App.tsx, examples/vue/virtualized-rows/src/App.vue.
3. External atoms (recommended for shared state)
Use createAtom from @tanstack/vue-store; pass through options.atoms. Atoms take
precedence over options.state — pick one mechanism per slice.
<script setup lang="ts">
import { ref } from 'vue'
import { createAtom, useSelector } from '@tanstack/vue-store'
import {
createPaginatedRowModel,
rowPaginationFeature,
tableFeatures,
useTable,
type PaginationState,
} from '@tanstack/vue-table'
const features = tableFeatures({
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
})
const paginationAtom = createAtom<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const pagination = useSelector(paginationAtom) // reactive ref-like
const data = ref([] as Person[])
const table = useTable({
features,
columns,
data,
atoms: { pagination: paginationAtom },
// NOTE: no `onPaginationChange` — `table.setPageIndex()` writes through to the atom.
})
</script>
<template>
<button @click="table.nextPage()" :disabled="!table.getCanNextPage()">
Next
</button>
<span>Page {{ pagination.value.pageIndex + 1 }}</span>
</template>
Source: examples/vue/basic-external-atoms/src/App.tsx.
4. External state + on[State]Change with getters
Still supported and convenient for migration paths. The critical rule: pass each slice as a
getter so Vue can track .value changes. A raw ref captured in the state object is read
once and never re-tracked.
const sorting = ref<SortingState>([])
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 10 })
const table = useTable({
features,
columns,
data,
state: {
get sorting() {
return sorting.value
}, // <- getter
get pagination() {
return pagination.value
}, // <- getter
},
onSortingChange: (u) => {
sorting.value = typeof u === 'function' ? u(sorting.value) : u
},
onPaginationChange: (u) => {
pagination.value = typeof u === 'function' ? u(pagination.value) : u
},
})
Source: examples/vue/basic-external-state/src/App.tsx,
docs/framework/vue/guide/table-state.md (External State).
5. <FlexRender> and table.Subscribe
<FlexRender> accepts a cell, header, or footer prop (preferred). The legacy
:render / :props pattern still works.
<FlexRender :cell="cell" />
<FlexRender :header="header" />
<FlexRender :footer="header" />
<!-- Legacy form -->
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
table.Subscribe mostly mirrors React. It is rarely the right tool in Vue — prefer
computed(() => table.atoms.<slice>.get()) or the useTable selector. If you do need it
(for cross-store source subscription), it accepts a source prop:
// In a render function or JSX file
return () => (
<table.Subscribe source={someAtom} selector={(v) => v.someField}>
{(value) => <span>{value}</span>}
</table.Subscribe>
)
Common Mistakes
Passing a ref to state.pagination without a getter (CRITICAL)
// ❌ Captures the ref object; later `pagination.value = …` writes are invisible.
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 10 })
const table = useTable({
features,
columns,
data,
state: { pagination },
})
// ✅ Use a getter so Vue tracks `.value`.
const table = useTable({
features,
columns,
data,
state: {
get pagination() {
return pagination.value
},
},
onPaginationChange: (u) => {
pagination.value = typeof u === 'function' ? u(pagination.value) : u
},
})
Source: docs/framework/vue/guide/table-state.md (External State).
Reading table.atoms.<slice>.get() outside a reactive context (HIGH)
// ❌ One-shot read at component setup — never updates.
const pagination = table.atoms.pagination.get()
// ✅ Wrap in `computed` so Vue tracks the atom.
const pagination = computed(() => table.atoms.pagination.get())
// or
const table = useTable(opts, (s) => ({ pagination: s.pagination }))
// then read `table.state.pagination`
Using the v8 useVueTable name (HIGH)
// ❌ Removed in v9.
import { useVueTable } from '@tanstack/vue-table'
// ✅
import { useTable } from '@tanstack/vue-table'
"API missing" because the feature is not in features (CRITICAL, v9-specific)
// ❌ `rowSortingFeature` not registered → `table.setSorting` and `table.atoms.sorting` do not exist.
const features = tableFeatures({})
const table = useTable({ features, columns, data })
table.setSorting([{ id: 'age', desc: true }]) // TS error / runtime no-op
// ✅ Register the feature and its matching row model factory inside tableFeatures.
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
})
const table = useTable({ features, columns, data })
Reimplementing built-in state transitions (CRITICAL — #1 AI tell)
// ❌ Hand-rolled sort — bypasses the table's invariants and reset APIs.
const sorting = ref<SortingState>([])
const sorted = computed(() => [...data.value].sort(/* … */))
// ✅ Let the table own it. Use `table.setSorting`, `column.toggleSorting`,
// `header.column.getToggleSortingHandler()`.
The library exposes setSorting, setColumnFilters, toggleSelected, nextPage, etc. for
nearly every state transition.
Hallucinating pre-v9 API names
useVueTable, getCoreRowModel() as an option, createColumnHelper<TData>() (single generic),
sortingFn instead of sortFn — all v8 shapes that will not compile. See
migrate-v8-to-v9 for the full rename list.
Unstable features / columns / data identity
Declare features, columnHelper, and columns outside <script setup> (at module
scope) or use computed. Recreating them every render churns the table's option diff watcher
and triggers a setOptions on every render — slow, and external atom slices can flicker.
See Also
tanstack-table/vue/getting-started— the end-to-end first-table walkthroughtanstack-table/vue/production-readiness— selectors, tree-shaking, identitytanstack-table/vue/compose-with-tanstack-store— external atoms in depthtanstack-table/table-core/state-management(core) — the atom model that drives this skilltanstack-table/vue/migrate-v8-to-v9—useVueTable→useTable