Tablecraft

Migrating to v4

What changes when you upgrade tablecraft to v4 and TanStack Table v9.

tablecraft v4 targets TanStack Table v9. There are five changes to make.

1. Upgrade the peer dependency

npm install @marvinackerman/tablecraft@^4 @tanstack/react-table@^9

Staying on v8 is supported: tablecraft 3.x remains published and installable.

2. Column pinning uses start/end

- const { pinLeft, pinRight, leftColumns, rightColumns } = columnPinning
+ const { pinStart, pinEnd, startColumns, endColumns } = columnPinning

isPinned returns 'start' | 'end' | false. The rename comes from v9 and is a correctness fix — "left" is wrong in a right-to-left layout. If you position pinned columns with CSS, inset-inline-start and inset-inline-end now match the vocabulary.

3. Type instances with TablecraftTable

- import type { Table } from '@tanstack/react-table'
- const table: Table<User> = useTable({ data, columns }).table
+ import type { TablecraftTable } from '@marvinackerman/tablecraft'
+ const table: TablecraftTable<User> = useTable({ data, columns }).table

v9's Table takes a feature set as its first type parameter. tablecraft binds that set for you, so you keep writing a single type argument.

4. Explicit ColumnDef/FilterFn annotations need TablecraftFeatures

v9's ColumnDef and FilterFn take a feature set as their first type parameter, the same way Table does above. tablecraft's own hooks already bind that parameter internally, so this only matters if you write the annotation out yourself instead of letting it be inferred:

- const columns: ColumnDef<User, any>[] = [...]
+ import type { TablecraftFeatures } from '@marvinackerman/tablecraft'
+ const columns: ColumnDef<TablecraftFeatures, User, any>[] = [...]

The same applies to a hand-written fuzzy filter function:

- const exactMatch: FilterFn<User> = (row, columnId, filterValue) => ...
+ import type { TablecraftFeatures } from '@marvinackerman/tablecraft'
+ const exactMatch: FilterFn<TablecraftFeatures, User> = (row, columnId, filterValue) => ...

Most consumers rely on inference — createColumns<User>([...]) or useTable<User>({...}) — and never write either annotation, so this change does not touch them. It hits only code that spelled out ColumnDef<User, ...> or FilterFn<User> explicitly.

5. columnsFromZod requires an object or array schema

v9 tightened RowData from unknown to Record<string, any> | Array<any>, and columnsFromZod follows: its schema parameter is now z.ZodType<RowData>.

A normal object schema is unaffected — this is the overwhelmingly common case:

// still fine
columnsFromZod(z.object({ id: z.number(), name: z.string() }))

A schema whose output is a primitive is now rejected at compile time:

// was: type-checked, then threw at runtime
// now: fails to compile
columnsFromZod(z.string())

Nothing that worked before stops working. The change converts a runtime failure into a compile-time one.

What does not change

Every hook name, every option and every return shape. useTable, useServerTable, useQueryTable, the ten granular state hooks, editing, export, faceting and accessibility are all called exactly as before. The one exception is change 4 above: if you annotate ColumnDef or FilterFn explicitly rather than relying on inference, that annotation's type changes.

Two things from v9 itself

These are TanStack renames rather than tablecraft ones, but you will meet them on the way through.

RowData is narrower. v9 tightened it from unknown to Record<string, any> | Array<any>. If your row type is a primitive, wrap it in an object.

VisibilityState is now ColumnVisibilityState. tablecraft does not re-export it, so if you imported it from @tanstack/react-table to type a visibility map, rename the import:

- import type { VisibilityState } from '@tanstack/react-table'
+ import type { ColumnVisibilityState } from '@tanstack/react-table'

On this page