Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/legacy-column-helper-fn-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-table': patch
---

Column defs built with `legacyCreateColumnHelper` now accept the built-in `filterFn`, `sortFn`, and `aggregationFn` names, matching the registries `useLegacyTable` registers at runtime.
77 changes: 48 additions & 29 deletions packages/react-table/src/useLegacyTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
FilterFns,
Header,
HeaderGroup,
Prettify,
Row,
RowData,
RowModel,
Expand Down Expand Up @@ -157,34 +158,47 @@ export function getCoreRowModel<
// Type definitions
// =============================================================================

/**
* Feature set registered by `useLegacyTable`.
*
* Extends the stock features with the built-in filter, sort, and aggregation
* registries so column definitions accept the v8 string identifiers such as
* `'mean'` and `'includesString'`.
*/
export interface LegacyFeatures extends StockFeatures {
aggregationFns: Prettify<AggregationFns & typeof aggregationFns>
filterFns: Prettify<FilterFns & typeof filterFns>
sortFns: Prettify<SortFns & typeof sortFns>
}

/**
* Row model factory function type from v8 API
*/
export type RowModelFactory<TData extends RowData> = (
table: Table<StockFeatures, TData>,
) => () => RowModel<StockFeatures, TData>
table: Table<LegacyFeatures, TData>,
) => () => RowModel<LegacyFeatures, TData>

/**
* Faceted row model factory function type from v8 API
*/
export type FacetedRowModelFactory<TData extends RowData> = (
table: Table<StockFeatures, TData>,
table: Table<LegacyFeatures, TData>,
columnId: string,
) => () => RowModel<StockFeatures, TData>
) => () => RowModel<LegacyFeatures, TData>

/**
* Faceted min/max values factory function type from v8 API
*/
export type FacetedMinMaxValuesFactory<TData extends RowData> = (
table: Table<StockFeatures, TData>,
table: Table<LegacyFeatures, TData>,
columnId: string,
) => () => undefined | [number, number]

/**
* Faceted unique values factory function type from v8 API
*/
export type FacetedUniqueValuesFactory<TData extends RowData> = (
table: Table<StockFeatures, TData>,
table: Table<LegacyFeatures, TData>,
columnId: string,
) => () => Map<any, number>

Expand Down Expand Up @@ -263,7 +277,7 @@ export interface LegacyRowModelOptions<TData extends RowData> {
* @deprecated This is a compatibility layer for migrating from v8. Use `useTable` with an explicit `features` option instead.
*/
export type LegacyTableOptions<TData extends RowData> = Omit<
TableOptions<StockFeatures, TData>,
TableOptions<LegacyFeatures, TData>,
'features'
> &
LegacyRowModelOptions<TData>
Expand All @@ -274,80 +288,80 @@ export type LegacyTableOptions<TData extends RowData> = Omit<
* @deprecated Use `useTable` with explicit state selection instead.
*/
export type LegacyReactTable<TData extends RowData> = ReactTable<
StockFeatures,
LegacyFeatures,
TData,
TableState<StockFeatures>
TableState<LegacyFeatures>
> & {
/**
* Returns the current table state.
* @deprecated In v9, access state directly via `table.state` or use `table.state` for the full state.
*/
getState: () => TableState<StockFeatures>
getState: () => TableState<LegacyFeatures>
/**
* Sets the current table state.
* @deprecated In v9, access state directly via `table.baseAtoms`
*/
setState: (state: TableState<StockFeatures>) => void
setState: (state: TableState<LegacyFeatures>) => void
}

// =============================================================================
// Legacy type aliases - StockFeatures hardcoded for simpler prop typing with useLegacyTable
// Legacy type aliases - LegacyFeatures hardcoded for simpler prop typing with useLegacyTable
// =============================================================================

/** @deprecated Use Column<TFeatures, TData, TValue> with useTable instead. */
export type LegacyColumn<TData extends RowData, TValue = unknown> = Column<
StockFeatures,
LegacyFeatures,
TData,
TValue
>

/** @deprecated Use Row<TFeatures, TData> with useTable instead. */
export type LegacyRow<TData extends RowData> = Row<StockFeatures, TData>
export type LegacyRow<TData extends RowData> = Row<LegacyFeatures, TData>

/** @deprecated Use Cell<TFeatures, TData, TValue> with useTable instead. */
export type LegacyCell<TData extends RowData, TValue = unknown> = Cell<
StockFeatures,
LegacyFeatures,
TData,
TValue
>

/** @deprecated Use Header<TFeatures, TData, TValue> with useTable instead. */
export type LegacyHeader<TData extends RowData, TValue = unknown> = Header<
StockFeatures,
LegacyFeatures,
TData,
TValue
>

/** @deprecated Use HeaderGroup<TFeatures, TData> with useTable instead. */
export type LegacyHeaderGroup<TData extends RowData> = HeaderGroup<
StockFeatures,
LegacyFeatures,
TData
>

/** @deprecated Use ColumnDef<TFeatures, TData, TValue> with useTable instead. */
export type LegacyColumnDef<
TData extends RowData,
TValue = unknown,
> = ColumnDef<StockFeatures, TData, TValue>
> = ColumnDef<LegacyFeatures, TData, TValue>

/** @deprecated Use Table<TFeatures, TData> with useTable instead. */
export type LegacyTable<TData extends RowData> = Table<StockFeatures, TData>
export type LegacyTable<TData extends RowData> = Table<LegacyFeatures, TData>

// =============================================================================
// Legacy column helper - StockFeatures hardcoded
// Legacy column helper - LegacyFeatures hardcoded
// =============================================================================

/**
* @deprecated Use `createColumnHelper<TFeatures, TData>()` with useTable instead.
*
* A column helper with StockFeatures pre-bound for use with useLegacyTable.
* A column helper with LegacyFeatures pre-bound for use with useLegacyTable.
* Only requires TData—no need to specify TFeatures.
*/
export function legacyCreateColumnHelper<TData extends RowData>(): ColumnHelper<
StockFeatures,
LegacyFeatures,
TData
> {
return createColumnHelper<StockFeatures, TData>()
return createColumnHelper<LegacyFeatures, TData>()
}

// =============================================================================
Expand Down Expand Up @@ -409,11 +423,16 @@ export function useLegacyTable<TData extends RowData>(
const [features] = useState(() => {
// Legacy row model options are setup-only. Capture the first render's
// marker options to match the table instance lifecycle.
const legacyFeatures: StockFeatures & Partial<TableFeatures> = {
const legacyFeatures: LegacyFeatures & Partial<TableFeatures> = {
...stockFeatures,
filterFns: { ...filterFns, ...options.filterFns },
sortFns: { ...sortFns, ...options.sortFns },
aggregationFns: { ...aggregationFns, ...options.aggregationFns },
// Declaration-merged names are required members of the registries, so the
// optional options are asserted; spreading `undefined` yields no keys.
filterFns: { ...filterFns, ...(options.filterFns as FilterFns) },
sortFns: { ...sortFns, ...(options.sortFns as SortFns) },
aggregationFns: {
...aggregationFns,
...(options.aggregationFns as AggregationFns),
},
}

if (getFilteredRowModel) {
Expand Down Expand Up @@ -452,7 +471,7 @@ export function useLegacyTable<TData extends RowData>(
})

// Call useTable with the v9 API, subscribing to all state changes
const table = useTable<StockFeatures, TData, TableState<StockFeatures>>(
const table = useTable<LegacyFeatures, TData, TableState<LegacyFeatures>>(
{
...restOptions,
features,
Expand All @@ -465,7 +484,7 @@ export function useLegacyTable<TData extends RowData>(
}, [table])

const setState = useCallback(
(state: TableState<StockFeatures>) => {
(state: TableState<LegacyFeatures>) => {
Object.entries(state).forEach(([key, value]) => {
// @ts-expect-error - baseAtoms is indexed by dynamic string keys
table.baseAtoms[key].set(value)
Expand Down
167 changes: 167 additions & 0 deletions packages/react-table/tests/useLegacyTable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// @vitest-environment jsdom

import { cleanup, renderHook } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import {
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
legacyCreateColumnHelper,
useLegacyTable,
} from '../src/useLegacyTable'
import type {
AggregationFnDef,
FilterFn,
RowData,
SortFn,
TableFeatures,
} from '@tanstack/table-core'

type Person = {
firstName: string
age: number
}

// v8-style declaration merging: the way custom fn names became valid string
// values before the registry slots existed. `useLegacyTable` still supports it.
declare module '@tanstack/table-core' {
interface FilterFns {
startsWithLetter: FilterFn<TableFeatures, RowData>
}
interface SortFns {
byNameLength: SortFn<TableFeatures, RowData>
}
interface AggregationFns {
range: AggregationFnDef<TableFeatures, RowData, unknown, number>
}
}

const startsWithLetter: FilterFn<TableFeatures, RowData> = (
row,
columnId,
filterValue,
) => String(row.getValue(columnId)).startsWith(String(filterValue))

const byNameLength: SortFn<TableFeatures, RowData> = (rowA, rowB, columnId) =>
String(rowA.getValue(columnId)).length -
String(rowB.getValue(columnId)).length

const range: AggregationFnDef<TableFeatures, RowData, unknown, number> = {
aggregate: (context) => {
const values = context.rows.map((row) => Number(context.getValue(row)))
return Math.max(...values) - Math.min(...values)
},
}

const columnHelper = legacyCreateColumnHelper<Person>()

const columns = columnHelper.columns([
columnHelper.accessor('firstName', { filterFn: 'includesString' }),
columnHelper.accessor('age', { aggregationFn: 'mean', sortFn: 'basic' }),
])

const customFnColumns = columnHelper.columns([
columnHelper.accessor('firstName', {
filterFn: 'startsWithLetter',
sortFn: 'byNameLength',
}),
columnHelper.accessor('age', { aggregationFn: 'range' }),
])

const data: ReadonlyArray<Person> = [
{ firstName: 'Tanner', age: 20 },
{ firstName: 'Kevin', age: 40 },
]

// Name length runs opposite to alphabetical order, so a sorted result can only
// come from `byNameLength`.
const customFnData: ReadonlyArray<Person> = [
{ firstName: 'Alexander', age: 20 },
{ firstName: 'Bo', age: 50 },
]

afterEach(() => {
cleanup()
})

describe('useLegacyTable', () => {
it('accepts built-in fn names on helper-built column defs', () => {
const { result } = renderHook(() =>
useLegacyTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
}),
)

expect(result.current.getColumn('age')!.getAggregationValue()).toBe(30)
})

it('applies the named filter fn to the filtered row model', () => {
const { result } = renderHook(() =>
useLegacyTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: { columnFilters: [{ id: 'firstName', value: 'ann' }] },
}),
)

expect(
result.current.getRowModel().rows.map((row) => row.getValue('firstName')),
).toEqual(['Tanner'])
})

it('applies the named sort fn to the sorted row model', () => {
const { result } = renderHook(() =>
useLegacyTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
initialState: { sorting: [{ id: 'age', desc: true }] },
}),
)

expect(
result.current.getRowModel().rows.map((row) => row.getValue('age')),
).toEqual([40, 20])
})

it('applies declaration-merged sort and aggregation fn names', () => {
const { result } = renderHook(() =>
useLegacyTable({
columns: customFnColumns,
data: customFnData,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
sortFns: { byNameLength },
aggregationFns: { range },
initialState: { sorting: [{ id: 'firstName', desc: false }] },
}),
)

expect(
result.current.getRowModel().rows.map((row) => row.getValue('firstName')),
).toEqual(['Bo', 'Alexander'])
expect(result.current.getColumn('age')!.getAggregationValue()).toBe(30)
})

it('applies the declaration-merged filter fn name', () => {
const { result } = renderHook(() =>
useLegacyTable({
columns: customFnColumns,
data: customFnData,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
filterFns: { startsWithLetter },
initialState: { columnFilters: [{ id: 'firstName', value: 'A' }] },
}),
)

expect(
result.current.getRowModel().rows.map((row) => row.getValue('firstName')),
).toEqual(['Alexander'])
})
})
Loading