Tables · Open source
react-table-v7-migrate
A codemod CLI that rewrites react-table v7 code (useTable with its plugin hooks, the prop-getter render loops and v7 column definitions) to TanStack Table v9, plus a /compat entry for the v7 pieces v9 has no exact match for. Whatever it cannot migrate safely gets a TODO comment and a line in the report.
What it solves
react-table 7.8.0, the last v7 release (May 2022), declares react ^16.8.3 || ^17.0.0-0 || ^18.0.0 as its peer range, so an app on v7 cannot move to React 19 with a plain npm install: npm stops with ERESOLVE.
The successor, TanStack Table, is now at v9 and its migration guide starts at v8. Moving by hand means rewriting every useTable call, every getTableProps / prepareRow render loop, every column definition and every renderer's props, while keeping the v7 behaviour users are used to.
Point the CLI at your source folder and it rewrites the files in place. useTable(options, useSortBy, useFilters, ...) becomes useTable({ features, ...options }) with a generated tableFeatures({...}) that registers the v9 features, row models and filter/sort functions the plugins need. The destructured instance becomes one const per name, the render loops lose their prop getters and get key and colSpan attributes, render('Header') and render('Cell') become flexRender, column keys are renamed (accessor, Header, Cell, filter, sortType, width, disable*), and renderer props are rewritten to the v9 context. Where v9's defaults differ from v7's, the v7 behaviour is written out (ascending-first sorting, the case-sensitive alphanumeric sort, the text filter, row.subRows, any row can expand, no Shift range-select). Grouping, resizing, the layout hooks, useRowState, stateReducer and a few other things are not migrated: they get a TODO comment and appear in the report. The /compat entry has useAsyncDebounce, wrappers for custom v7 filter and sort types, and v7's includes filter and number sort. Tested on 13 react-table 7.8.0 example apps, rendered before and after on React 19, and on a real Next.js app checked in a browser. An independent package, not made or endorsed by TanStack or the react-table authors.
- Migrates useTable with useSortBy, useFilters, useGlobalFilter, usePagination, useRowSelect, useExpanded and useColumnOrder to useTable({ features }) and a generated tableFeatures({...}); columnVisibilityFeature is always added
- Rewrites the render loop: getTableProps, getTableBodyProps and prepareRow are removed, getHeaderGroupProps / getRowProps / getCellProps / getHeaderProps become key (and colSpan) attributes, object arguments are merged like v7's mergeProps
- Toggle prop getters (sorting, row and page selection, expanding, column hiding) become the v9 handler and state, with v7's title and pointer cursor
- Maps 53 v7 table-instance names (page, gotoPage, canNextPage, selectedFlatRows, setGlobalFilter, ...), state and initialState keys, and table options (manualSortBy, autoResetPage, disableMultiSort, ...); unknown options move to meta
- Column definitions and renderer props: accessor to accessorKey / accessorFn, Header / Cell / Footer to header / cell / footer, Filter to meta.Filter, custom keys to meta, Cell: ({ value, row }) to the v9 cell context
- Built-in v7 filter and sort types mapped to v9 names; includes and number, which v9 has no exact match for, and custom filterTypes / sortTypes go through /compat
- Keeps v7 behaviour where v9 defaults differ: sortDescFirst: false, a case-sensitive alphanumeric default sort, the text filter for columns and the global filter, getSubRows, getRowCanExpand: () => true, enableRowRangeSelection: false
- TypeScript: v7 types become v9 types with the features type, and a columns-only file is linked to the features of the table file it imports (relative imports and tsconfig baseUrl / paths)
- TODO comments plus a per-file report (text or --json) for everything not migrated; --dry and --print preview without writing; a second run changes nothing
- /compat entry: useAsyncDebounce, fromV7FilterType(s), fromV7SortType(s), filterFn_v7Includes, sortFn_v7Number, toV7Row
Install
Requires TanStack Table v9 (React).
npx @amjadiqbal/react-table-v7-migrate <paths>
Usage
# Commit first: the codemod rewrites files in place.
# Preview: prints the report, writes nothing
npx @amjadiqbal/react-table-v7-migrate --dry src
# Rewrite
npx @amjadiqbal/react-table-v7-migrate src
# Swap the dependencies
npm uninstall react-table @types/react-table
npm install @tanstack/react-table@^9
# Only if the migrated code imports /compat (the report says so)
npm install @amjadiqbal/react-table-v7-migrate
Setup
The CLI runs with npx and needs nothing installed in your app; its own dependencies are @babel/parser, recast and tinyglobby. The package is ESM only and needs Node 20 or later. Peer dependencies are react >=18 and @tanstack/react-table >=9.0.0, both marked optional: only the /compat entry imports react, and the CLI imports neither. The codemod does not format its output: generated code follows the file's quote style but always adds semicolons.
- 1 Commit or back up your work. Files are rewritten in place.
- 2 npx @amjadiqbal/react-table-v7-migrate --dry src to see the report without writing anything.
- 3 npx @amjadiqbal/react-table-v7-migrate src. Run it on the whole source folder at once, so TypeScript column files can be linked to their table file.
- 4 npm uninstall react-table @types/react-table, then npm install @tanstack/react-table@^9.
- 5 If the migrated code imports @amjadiqbal/react-table-v7-migrate/compat, npm install @amjadiqbal/react-table-v7-migrate as a regular dependency.
- 6 Work through the TODO(react-table-v7-migrate) comments, then run Prettier or your linter's fixer, type-check and test.
CLI
react-table-v7-migrate [options] <paths or globs...>. The options are --dry (report only, write nothing), --print (print each changed file's new code to stdout; implies --dry), --json (the report as JSON) and -h / --help. There are no options for extensions, the parser or ignore patterns.
- A directory is searched for .js, .jsx, .ts, .tsx, .mjs, .cjs, .mts and .cts files. An argument that contains *, ?, { or [ is used as a glob, and a path ending in one of those extensions as a file.
- node_modules, dist and build are always skipped, and files whose text does not contain react-table are skipped.
- The parser follows the extension: TypeScript for .ts / .tsx / .mts / .cts, JSX for everything except .ts / .mts / .cts. Flow is not supported; a file that does not parse is reported as an error.
- Changed files are written back in place. With --print the code goes to stdout and the report to stderr.
- The report lists each changed or noted file with how many useTable calls were migrated, the v9 features registered, and every TODO and note with its line. --json prints the same without the code.
- Exit code 1 when a file could not be parsed or transformed, and also when no path is given (help is printed); 0 otherwise, TODOs included.
- A second run changes nothing: once a file imports from @tanstack/react-table instead of react-table, the codemod leaves it alone.
- Programmatic use: transformSource(source, { filename }) from the main entry returns { code, changed, notes, features, tables }. Without filename it parses as .tsx.
What is migrated
useTable calls inside a function component, with the options passed as an object literal, are rewritten fully. The plugin hooks useSortBy, useFilters, useGlobalFilter, usePagination, useRowSelect, useExpanded and useColumnOrder become rowSortingFeature, columnFilteringFeature, globalFilteringFeature, rowPaginationFeature, rowSelectionFeature, rowExpandingFeature and columnOrderingFeature, with their row models, in one tableFeatures({...}) call. It is module-level, or created with useMemo inside the component when filterTypes / sortTypes are defined there.
- Instance: const { headerGroups, rows, page, ... } = useTable(...) becomes const table = useTable(...) and one const per name (const page = table.getRowModel().rows, const gotoPage = table.setPageIndex). The same applies to const instance = useTable(...) and instance.x reads. With usePagination, rows and selectedFlatRows read the pre-paginated rows, as in v7.
- hooks => { hooks.visibleColumns.push(fn) } (the selection checkbox column) becomes columns: useMemo(() => fn(columns), [columns]). Other plugin hook code gets a TODO.
- State: state.pageIndex, sortBy, filters, globalFilter, selectedRowIds, expanded, hiddenColumns and columnOrder read from table.state. initialState keys are renamed (pagination: { pageIndex, pageSize }, sorting, columnFilters, rowSelection, columnVisibility).
- Options: manualSortBy, manualFilters, manualGlobalFilter, autoResetPage and selectSubRows are renamed; disableMultiSort, disableSortRemove, disableMultiRemove, disableSortBy, disableFilters and disableGlobalFilter become enable* = negated; v7 autoReset* options v9 lacks are dropped with a note; any other option moves to meta (table.options.meta?.updateMyData).
- Render loop: getTableProps(), getTableBodyProps() and prepareRow(row) are removed; the element prop getters become key (and colSpan for headers) attributes; column.render('Header' / 'Footer' / 'Filter') and cell.render('Cell' / 'Aggregated') become flexRender calls; the header loop variable is renamed from column to header when the name is free.
- Row, cell and column members: row.cells, row.values.x, row.isSelected, row.isExpanded, row.canExpand, toggleRowSelected, toggleRowExpanded, cell.value, column.isSorted, isSortedDesc, canSort, canFilter, filterValue, setFilter, preFilteredRows (via columnFacetingFeature), isVisible, toggleHidden.
- Column definitions: accessor: 'a.b' becomes accessorKey plus id: 'a.b', accessor functions become accessorFn, Header / Cell / Footer / Filter become header / cell / footer / meta.Filter, filter / sortType / sortInverted become filterFn / sortFn / invertSorting, width / minWidth / maxWidth become size / minSize / maxSize, disableSortBy / disableFilters / disableGlobalFilter become enable* = false, other keys move to meta.
- Renderer props: Cell: ({ value, row, column, cell }), Header: ({ getToggleAllRowsSelectedProps, rows, state }), props.value and Filter components ({ column: { filterValue, setFilter, preFilteredRows, id } }) are rewritten to the v9 context, for inline renderers and components defined in the same file.
- Filter types text, exactText, exactTextCase, includesAll, includesSome, includesValue, exact, equals and between map to includesString, equalsString, equalsStringSensitive, arrIncludesAll, arrIncludesSome, arrHas, equals, weakEquals and inNumberRange. Sort types alphanumeric, datetime, basic and string map to alphanumericCaseSensitive, datetime, basic and text.
v7 behaviour the codemod keeps
Where v9's defaults differ from v7's, the codemod writes the v7 behaviour out as options, so the migrated table behaves the same. Delete them when you want v9's defaults.
- With useSortBy: sortDescFirst: false (v9 sorts number columns descending first) and a v7DefaultColumn with sortFn: 'alphanumericCaseSensitive' (v9's alphanumeric lowercases).
- With useFilters: filterFn: 'includesString' on every column; with useGlobalFilter: globalFilterFn: 'includesString'. v7 used its built-in text filter here even when filterTypes overrode text.
- getSubRows: (row) => row.subRows, added when the table uses expanding or row selection, or the file mentions subRows.
- With useExpanded: getRowCanExpand: () => true, so sub-component rows still expand. row.canExpand still means "has sub rows".
- With useRowSelect: enableRowRangeSelection: false, since v9 selects a range on Shift-click.
- Not papered over: in a descending sort v7 put equal values in reverse original order and v9 keeps their order; expand-all stores expanded: true; column.preFilteredRows becomes the faceted rows, whose order can differ; v9 does not reset sorting, filters or selection when data changes; empty headers render nothing instead of a non-breaking space; table.state has v9's shape.
Left with a TODO
These are not migrated. The codemod writes a // TODO(react-table-v7-migrate): comment where it found them and lists each in the report.
- useGroupBy and grouping members (aggregate, Aggregated, canGroupBy, isGrouped, isAggregated): keys are renamed where v9 has a name, but the grouping feature and row model are not set up.
- useResizeColumns, useBlockLayout, useFlexLayout, useAbsoluteLayout, useGridLayout, useRowState and _UNSTABLE_usePivotColumns: the hook is removed from the call and the TODO says what to do instead.
- stateReducer, useControlledState, orderByFn, expandSubRows, manualExpandedKey, manualRowSelectedKey, getResetPageDeps, and plugin hook code other than hooks.visibleColumns.push.
- useTable options or initialState passed as a variable instead of an object literal (keys not renamed), a useTable call outside a function, or a result not assigned to a variable.
- Prop-getter function arguments (getCellProps((props, meta) => ...)) and prop getters passed by name.
- Renderers wrapped in React.memo, forwardRef or a HOC get a TODO; renderers imported from another file get a note. Either way their props are not rewritten.
- Accessor functions that use v7's third argument ({ subRows, depth, data }), and custom filter or sort type names that are not registered.
- import * as RT from 'react-table' and default imports: the file is left unchanged. Files with declare module 'react-table' are left unchanged; delete them after migrating.
TypeScript
v7 types are replaced with v9 types that take the features type first.
- Column becomes ColumnDef<typeof features, D, any>; ColumnInstance, TableInstance, TableOptions, TableState, Row, Cell and HeaderGroup keep their names with the features type; CellProps becomes CellContext; HeaderProps and FilterProps become HeaderContext; SortingRule becomes ColumnSort; Filters becomes ColumnFiltersState; IdType becomes string. Other v7 types get a TODO.
- useTable<User>(...) loses its type argument; v9 infers it from features and columns.
- Parameters of arrow functions the codemod generates are typed any, so noImplicitAny code still compiles.
- A file that only defines columns needs the table's features type. When the table file is in the same run, the table file exports its features and the column file gets import type { features } from the specifier it already uses; relative imports and tsconfig / jsconfig baseUrl and paths are followed. Otherwise the column file uses any and gets a TODO.
The /compat entry
@amjadiqbal/react-table-v7-migrate/compat holds the v7 pieces v9 has no exact match for. It imports react and does not load the codemod. The codemod adds these imports itself when a file needs them.
- useAsyncDebounce(fn, wait = 0): v7's hook. It returns a function that returns a promise; each call restarts the wait, and when it ends fn runs once with the last call's arguments and every pending promise resolves (or rejects) with that result. v7.8.0's own useAsyncDebounce needs a global regeneratorRuntime; this one does not.
- fromV7FilterType(fn) / fromV7FilterTypes(obj): wraps a v7 rows-level filter (rows, ids, filterValue) => rows as a v9 row filter. It calls fn once per row with that row alone, as a v7-style row with values, and keeps autoRemove. Filters that need all rows at once (ranking, top N) do not work this way.
- fromV7SortType(fn) / fromV7SortTypes(obj): wraps a v7 sort type (rowA, rowB, columnId, desc). desc is passed as undefined; v9 applies the direction itself.
- filterFn_v7Includes: v7's includes filter, value.includes(filterValue) with a single filter value; removed when the value is empty.
- sortFn_v7Number: v7's number sort, keeping only digits and dots before comparing as numbers.
- toV7Row(row): a view of a v9 row with a v7 values object read through row.getValue().
import { useAsyncDebounce } from '@amjadiqbal/react-table-v7-migrate/compat';
const onChange = useAsyncDebounce((value) => {
table.setGlobalFilter(value || undefined);
}, 200);
Compatibility and testing
Tested with react-table 7.8.0, @tanstack/react-table 9.3.1 and React 19.3.1 on Node 26.7 and npm 11.19. Not tested: v9 on React 18, the grouping, resizing and layout plugins (not migrated), and Flow (not supported). react-table-v7-migrate is an independent package and is not made or endorsed by TanStack or the react-table authors.
- Peer ranges: react >=18 and @tanstack/react-table >=9.0.0, both optional. react-table 7.8.0 itself declares react ^16.8.3 || ^17.0.0-0 || ^18.0.0.
- 56 Vitest tests: the output and report of 11 focused cases and 14 react-table v7.8.0 examples (including kitchen-sink, which uses grouping) compared with reviewed snapshots, each run a second time to check nothing changes; the TypeScript case's output compiled with tsc --strict against @tanstack/react-table 9.3.1; the CLI; the compat helpers; and the README example against the real codemod output.
- 13 react-table v7.8.0 example apps (basic, sorting, filtering, pagination, pagination-controlled, row-selection, row-selection-and-pagination, expanding, footers, column-hiding, column-ordering, editable-data, sub-components): the original on react-table 7.8.0 and the migrated output on v9 render side by side on React 19 in jsdom, the same scripted clicks, typing and select changes run on both, and every table cell, checkbox, input and state dump is compared after each step. The only relaxations are the documented differences: tie order, expanded: true, faceted-row order and whole-state dumps.
- A real app, alfianandinugraha/next-tabley (Next.js, TypeScript, Chakra UI; sorting, column filters, global filter, pagination, row selection, hooks.visibleColumns and an @types/react-table augmentation), its toolchain upgraded by hand to Next 16.4.2 and React 19.3.1: the codemod changed 3 files and left 1 TODO (delete the type augmentation file), tsc --noEmit was clean and next build succeeded. In the browser the migrated page and the untouched v7 page (react-table 7.8.0 on React 19) gave identical table snapshot hashes after every step: sort ascending and descending, a column filter, the global search, Next, page size 25, two row checkboxes, select page, clear, page size 5 and page 3 (11 hashes), with no console messages.
- Fresh install after publishing: on a new copy of next-tabley, npx fetched 0.1.0 from the npm registry with an empty cache and produced the same report; @tanstack/react-table 9.3.1 and the package itself were then installed from the registry for the /compat entry, whose useAsyncDebounce the app's filter and search use. tsc and next build were clean and the 11 table hashes matched the v7 page.
- Dry runs only (report, output not built) on five more public v7 apps, from 1 file and 0 TODOs up to 12 files and 38 TODOs for an app built on grouping, resizing and flex layout.
- Not migrated or tested: grouping and aggregation, column resizing, the layout hooks, useRowState and pivoting; renderers imported from other files or wrapped in memo / forwardRef; non-literal options; Flow files.
Hire me
Need this built into your app, or something custom?
I wrote react-table-v7-migrate. I can wire it into your TanStack app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More TanStack packages
All packages
Query Key Kit
Declare your TanStack Query v5 keys once as a typed tree, get queryOptions()-shaped objects back, and invalidate or update whole branches of the cache with typed helpers.
Start SEO Kit
Typed SEO helpers for TanStack Start: one head() call for title, canonical, Open Graph, Twitter, robots and hreflang tags, a sitemap and robots.txt served from server routes, typed JSON-LD with Product schema, and an optional Open Graph image route.
Table URL State
Keep a TanStack Table v9 table's sorting, column filters, global filter, pagination and column visibility in the URL search params, in both directions, and export the current view to CSV or, from a separate entry point, to XLSX.
AI SDK Bridge
A third-party migration stepping stone: run Vercel AI SDK provider models (@ai-sdk/* and community providers, provider spec v2, v3 and v4) as TanStack AI 1.0 adapters for chat, embeddings, transcription, speech, image generation and rerank, then swap each one for its native TanStack AI adapter.
shopify-app-tanstack-start
Build embedded Shopify apps on TanStack Start with the server API of Shopify's React Router package: session tokens and token exchange, webhook and app proxy verification, the mandatory compliance webhooks and the Billing API, as Start middleware, server functions and server routes.
Table Server Query
Turn TanStack Table v9 sorting, column filters, global search and pagination into server-side queries for Drizzle (Postgres, SQLite), Prisma, Kysely and Supabase/PostgREST, checked against a column allow-list and returned as rows plus a total count.
firebase-auth-tanstack-start
Firebase Authentication for TanStack Start with HttpOnly Firebase session cookies: the browser signs in with the Firebase JS SDK, and the server verifies the session cookie or a bearer ID token and puts a typed user with custom claims on the context for server rendering, server functions, server routes and beforeLoad guards.
TanStack AI RAG
A third-party RAG kit for TanStack AI 1.x: token-aware text and Markdown chunking with overlap, ingest through TanStack AI embed() with atomic re-ingest, in-memory and Postgres pgvector stores (pg, postgres.js, PGlite, Drizzle), metadata-filtered retrieval, optional rerank through rerank(), and a server tool for chat() whose citation numbers continue across searches in one run.