Skip to content

feat: new custom plugins support and massive typescript simplificatoins#6305

Merged
KevinVandy merged 4 commits into
betafrom
plugin-better
Jun 8, 2026
Merged

feat: new custom plugins support and massive typescript simplificatoins#6305
KevinVandy merged 4 commits into
betafrom
plugin-better

Conversation

@KevinVandy

@KevinVandy KevinVandy commented Jun 8, 2026

Copy link
Copy Markdown
Member

🎯 Changes

This PR adds first-class custom table plugin support while simplifying and tightening the TypeScript model across core features, adapters, devtools, and examples. It updates custom-feature docs and React/Preact/Angular plugin examples, improves feature-map typing for table/core APIs, fixes built-in sort/filter/aggregation function typing, makes devtools registration work with concrete table instances, restores proper contextual typing for legacy column helpers, and updates affected React/Svelte/Vue/Angular example types to use the real TanStack Table feature types instead of workaround shapes.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

Summary by CodeRabbit

Release Notes

  • New Features

    • Simplified custom table feature authoring with direct feature exports instead of factory functions
    • Enhanced TypeScript type safety with stricter feature registration patterns
  • Documentation

    • Updated custom features guide with revised TypeScript examples
  • Examples

    • Updated React, Angular, Preact, and Svelte examples to reflect new feature patterns
    • Improved type declarations in kitchen-sink examples
  • Refactor

    • Streamlined feature registration API across all adapters
    • Removed plugin-based type augmentation for cleaner composition

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR rewrites table feature typing around feature maps, removes several plugin-merging type layers, converts many core features to direct TableFeature exports, and updates example integrations, devtools types, docs, and tests to match the new contracts.

Changes

Feature-map typing refactor

Layer / File(s) Summary
Public type contracts
packages/table-core/src/types/*, packages/table-core/src/fns/*, packages/table-core/src/utils.ts
Table, TableState, TableOptions, Row, Column, Cell, Header, HeaderGroup, row-model types, and TableFeature now derive feature typing from feature maps; built-in aggregation, filter, and sort function signatures are updated.
Core feature exports
packages/table-core/src/core/*, packages/table-core/src/features/*
Core table/column/row/header/pagination/pinning/selection/sorting/filtering/faceting/resizing/sizing/visibility/expanding features are exported directly as TableFeature objects, and internal row-model helpers and casts are adjusted to the new typing.
Examples, devtools, and docs
docs/guide/custom-features.md, examples/*, packages/*devtools*, packages/vue-table/*, packages/angular-table/*
Custom-feature docs, Angular/Preact/React/Svelte example plugins, devtools type surfaces, and Vue render props are retyped to the new feature-map and table contracts.
Test typing updates
packages/table-core/tests/*, packages/angular-table/tests/*
Test helpers and feature tests switch to internal table types, typed feature maps, and updated nullability/casting patterns.
Utility and type cleanup
packages/table-core/src/core/headers/buildHeaderGroups.ts, packages/table-core/src/core/table/coreTablesFeature.utils.ts, packages/table-core/src/core/row-models/coreRowModelsFeature.types.ts
Several internal casts, helper typings, and removed plugin marker interfaces are aligned with the new type model.

Sequence Diagram(s)

sequenceDiagram
  participant CustomFeatureDocs
  participant ExamplePlugins
  participant CoreTableTypes
  participant FeatureWiring
  participant DevtoolsAndTests
  CustomFeatureDocs->>CoreTableTypes: describe new TableFeature and feature-map contracts
  ExamplePlugins->>FeatureWiring: register plugin APIs with assignTableAPIs
  FeatureWiring->>CoreTableTypes: consume TableFeature, TableState_All, and feature-map types
  DevtoolsAndTests->>CoreTableTypes: use updated table, row, column, and devtools types
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • TanStack/table#6299: Refactors the same table feature/type system toward feature maps and ExtractFeatureMapTypes, which matches the core typing direction used here.

Suggested reviewers

  • schiller-manuel

Poem

A rabbit hops through type-safe trails,
With feature maps and no old veils.
Table paws now land just right,
Devtools, docs, and tests all shine bright.
Hoppity hop, the types align 🐰

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch plugin-better

@nx-cloud

nx-cloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit a1a2e20

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ✅ Succeeded 3m 22s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 25s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-08 15:19:08 UTC

@KevinVandy KevinVandy changed the title Plugin better feat: new custom plugins support and massive typescript simplificatoins Jun 8, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-table

npm i https://pkg.pr.new/TanStack/table/@tanstack/angular-table@6305

@tanstack/angular-table-devtools

npm i https://pkg.pr.new/TanStack/table/@tanstack/angular-table-devtools@6305

@tanstack/lit-table

npm i https://pkg.pr.new/TanStack/table/@tanstack/lit-table@6305

@tanstack/match-sorter-utils

npm i https://pkg.pr.new/TanStack/table/@tanstack/match-sorter-utils@6305

@tanstack/preact-table

npm i https://pkg.pr.new/TanStack/table/@tanstack/preact-table@6305

@tanstack/preact-table-devtools

npm i https://pkg.pr.new/TanStack/table/@tanstack/preact-table-devtools@6305

@tanstack/react-table

npm i https://pkg.pr.new/TanStack/table/@tanstack/react-table@6305

@tanstack/react-table-devtools

npm i https://pkg.pr.new/TanStack/table/@tanstack/react-table-devtools@6305

@tanstack/solid-table

npm i https://pkg.pr.new/TanStack/table/@tanstack/solid-table@6305

@tanstack/solid-table-devtools

npm i https://pkg.pr.new/TanStack/table/@tanstack/solid-table-devtools@6305

@tanstack/svelte-table

npm i https://pkg.pr.new/TanStack/table/@tanstack/svelte-table@6305

@tanstack/table-core

npm i https://pkg.pr.new/TanStack/table/@tanstack/table-core@6305

@tanstack/table-devtools

npm i https://pkg.pr.new/TanStack/table/@tanstack/table-devtools@6305

@tanstack/vue-table

npm i https://pkg.pr.new/TanStack/table/@tanstack/vue-table@6305

@tanstack/vue-table-devtools

npm i https://pkg.pr.new/TanStack/table/@tanstack/vue-table-devtools@6305

commit: fd4421a

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/table-core/src/fns/filterFns.ts (1)

293-302: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

filterFn_arrHas is missing autoRemove, unlike other array filters.

All other array filters (arrIncludes, arrIncludesAll, arrIncludesSome) have autoRemove: (val) => testFalsy(val) || !val?.length to remove the filter when the array is empty. filterFn_arrHas lacks this, which could cause unexpected behavior when users clear the filter array.

🔧 Suggested fix for consistency
 export const filterFn_arrHas = Object.assign(
+  <TFeatures extends TableFeatures, TData extends RowData>(
-export const filterFn_arrHas = <
-  TFeatures extends TableFeatures,
-  TData extends RowData,
->(
   row: Row<TFeatures, TData>,
   columnId: string,
   filterValue: Array<unknown>,
-) => {
+  ) => {
   return filterValue.some((val) => row.getValue<unknown>(columnId) === val)
-}
+  },
+  { autoRemove: (val: any) => testFalsy(val) || !val?.length },
+)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/src/fns/filterFns.ts` around lines 293 - 302, The
filterFn_arrHas export is missing the autoRemove behavior used by other array
filters; update the exported filter (filterFn_arrHas) to include an autoRemove
property set to (val) => testFalsy(val) || !val?.length (matching
arrIncludes/arrIncludesAll/arrIncludesSome) so the filter is auto-removed when
the value is falsy or an empty array; import or reuse the testFalsy helper if
needed and ensure the exported shape matches the other array filter fns.
🧹 Nitpick comments (6)
packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts (1)

42-42: 💤 Low value

as any cast bypasses column accessor typing.

The cast silences type checking on the accessor options. If the typing issue is real (e.g., typedKey doesn't satisfy the accessor's id constraint), this could mask a problem.

♻️ Alternative: Remove cast if types align
-    return columnHelper.accessor(typedKey, { id: typedKey } as any)
+    return columnHelper.accessor(typedKey, { id: typedKey })

If the cast is necessary, add a comment explaining why the type system requires it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts`
at line 42, The test is silencing TypeScript by casting the accessor options to
any in columnHelper.accessor(typedKey, { id: typedKey } as any); either remove
the as any cast and make typedKey satisfy the accessor's id type (adjust the
typedKey declaration or the generic/type parameter used by
columnHelper.accessor) or if the cast is truly unavoidable add a brief comment
explaining why the accessor's options typing cannot be satisfied here and
reference the specific constraint being bypassed; ensure the change is applied
at the call site using columnHelper.accessor and/or the typedKey definition so
the typing intent is preserved.
docs/guide/custom-features.md (1)

242-261: 💤 Low value

Consider clarifying the table_ prefix convention.

The API keys use a table_ prefix (table_setDensity, table_toggleDensity), but users call the methods without this prefix (table.setDensity()). While this internal convention is consistent with the codebase, a brief inline comment explaining that assignTableAPIs strips the prefix would help users understand the mapping.

For example:

// assignTableAPIs strips the 'table_' prefix when registering methods
assignTableAPIs('densityPlugin', table, {
  table_setDensity: { ... }  // → becomes table.setDensity()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guide/custom-features.md` around lines 242 - 261, Add a brief inline
comment above the assignTableAPIs call explaining the internal naming
convention: that keys prefixed with "table_" (e.g., table_setDensity,
table_toggleDensity) are registered as instance methods without the prefix
(e.g., table.setDensity, table.toggleDensity) by assignTableAPIs; reference the
assignTableAPIs invocation and the specific keys (table_setDensity,
table_toggleDensity) and mention that these map to table.options handlers like
table.options.onDensityChange so readers understand the mapping.
examples/angular/custom-plugin/src/app/density/density-feature.ts (1)

81-89: 💤 Low value

Redundant wrapper in table_setDensity.

The safeUpdater wrapping in table_setDensity doesn't add any transformation—it just calls functionalUpdate(updater, old) and returns the result unchanged. You can pass updater directly to onDensityChange.

♻️ Suggested simplification
       table_setDensity: {
         fn: (updater: Updater<DensityState>) => {
-          const safeUpdater: Updater<DensityState> = (old) => {
-            const newState = functionalUpdate(updater, old)
-            return newState
-          }
-          return table.options.onDensityChange?.(safeUpdater)
+          return table.options.onDensityChange?.(updater)
         },
       },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/angular/custom-plugin/src/app/density/density-feature.ts` around
lines 81 - 89, The table_setDensity implementation contains a redundant
safeUpdater wrapper that merely calls functionalUpdate(updater, old) and returns
it; simplify by passing the original updater directly to
table.options.onDensityChange instead of creating safeUpdater (update the
table_setDensity fn to call table.options.onDensityChange?.(updater)), removing
the unnecessary safeUpdater and its use of functionalUpdate in this path.
examples/preact/custom-plugin/src/main.tsx (1)

77-118: 💤 Low value

Consider simplifying the redundant safeUpdater wrapper in table_setDensity.

The safeUpdater function on lines 101-104 adds no transformation—it just forwards the result of functionalUpdate. This could be simplified to pass the updater directly.

♻️ Optional simplification
       table_setDensity: {
         fn: (updater: Updater<DensityState>) => {
-          const safeUpdater: Updater<DensityState> = (old) => {
-            const newState = functionalUpdate(updater, old)
-            return newState
-          }
-          return table.options.onDensityChange?.(safeUpdater)
+          return table.options.onDensityChange?.(updater)
         },
       },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/preact/custom-plugin/src/main.tsx` around lines 77 - 118, The
safeUpdater wrapper in densityPlugin -> constructTableAPIs for table_setDensity
is redundant: instead of creating const safeUpdater that just computes
functionalUpdate(updater, old) and returns it, pass the provided updater
directly into the table options handler; replace the block in
table_setDensity.fn so it calls table.options.onDensityChange?.(updater)
(preserving the Updater<DensityState> type) rather than wrapping it in
safeUpdater. This removes unnecessary indirection while keeping behavior
identical.
packages/table-core/src/core/table/constructTable.ts (1)

118-125: 💤 Low value

Non-null assertion relies on initialization invariant.

Line 125 uses table.baseAtoms[key]!.get() with a non-null assertion. This assumes every key in stateKeys has a corresponding base atom. Since base atoms are created for each key in lines 106-114, this is safe—but the invariant depends on stateKeys being derived from table.initialState immediately before the atom creation loop.

Consider adding a runtime assertion in development mode to catch violations of this invariant if the code is refactored:

Optional defensive check
       const externalAtom = externalAtoms?.[key]
       if (externalAtom) {
         return externalAtom.get()
       }
+      if (process.env.NODE_ENV === 'development' && !table.baseAtoms[key]) {
+        throw new Error(`Missing base atom for state key: ${String(key)}`)
+      }
       return table.baseAtoms[key]!.get()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/src/core/table/constructTable.ts` around lines 118 - 125,
The non-null assertion on table.baseAtoms[key]!.get() in constructTable assumes
base atoms were created for every key in stateKeys; add a runtime defensive
check (only in development) before calling .get(): verify table.baseAtoms[key]
exists and throw a clear error or use console.assert/invariant naming the
missing key and referencing stateKeys and the atom-creation loop (the code that
builds baseAtoms) so refactors will surface the invariant violation early;
update the code path that reads externalAtoms/externalAtom to use this check and
keep behavior unchanged in production.
packages/angular-table/src/helpers/createTableHook.ts (1)

470-477: injectAppTable casts: avoid unknown by tightening the defaultTableOptions/TData typing

The object passed into injectTable<TFeatures, TData> is constructed from defaultTableOptions (coming from CreateTableContextOptions, which is based on TableOptions<TFeatures, any>) plus tableOptions() (which omits features/rowModels). That TData=any source is the real reason for the as unknown as TableOptions<TFeatures, TData> bridge at lines 470-470-ish—so the cast isn’t obviously masking a runtime shape mismatch, but it does bypass type-checking across the TData generic boundary.

The final cast to AppAngularTable<...> is supported by runtime: appTableFeatures.constructTableAPIs assigns appCell/appHeader/appFooter onto the created table.

No angular-table tests/specs cover this injectAppTable typing path, so to reduce risk, tighten the intermediate typing to eliminate the unknown casts (e.g., make defaultTableOptions preserve TData instead of any).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/angular-table/src/helpers/createTableHook.ts` around lines 470 -
477, The current casts around injectTable/injectAppTable hide a TData=any leak
from defaultTableOptions/CreateTableContextOptions; fix by making
defaultTableOptions and CreateTableContextOptions generic over TData (i.e.,
preserve TableOptions<TFeatures, TData> instead of defaulting to any) so the
constructed object already matches TableOptions<TFeatures, TData>, then remove
the intermediate "as unknown as TableOptions..." and final cast to
AppAngularTable; adjust the injectTable<>/injectAppTable<> call sites and the
signature of defaultTableOptions (and any helper that builds it) so
appTableFeatures.constructTableAPIs can still assign appCell/appHeader/appFooter
without needing unsafe casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/table-core/src/features/column-sizing/columnSizingFeature.ts`:
- Around line 93-103: The memoization dependencies for the registered selector
header_getStart include an unnecessary position argument; update the
registration in columnSizingFeature so memoDeps for header_getStart only depends
on header-related atoms (remove position from memoDeps array) OR alternatively,
if header offsets should vary by position/pinning, update the header_getStart
utility to accept and propagate position and then keep position in
memoDeps—refer to the header_getStart registration and the header_getStart
function to implement the chosen fix.

In `@packages/table-core/src/fns/filterFns.ts`:
- Around line 127-147: The comparison filter fns are using resolveFilterValue
incorrectly; replace the object passed to Object.assign in filterFn_greaterThan,
filterFn_greaterThanOrEqualTo, filterFn_lessThan, and filterFn_lessThanOrEqualTo
so they use the autoRemove hook instead of resolveFilterValue — e.g. change {
resolveFilterValue: (val:any) => testFalsy(val) } to { autoRemove: testFalsy }
(refer to the symbols filterFn_greaterThan, filterFn_greaterThanOrEqualTo,
filterFn_lessThan, filterFn_lessThanOrEqualTo to locate each assignment).

In `@packages/table-core/tests/helpers/generateTestTable.ts`:
- Line 37: The test helper is using a double-cast ("as any) as unknown as
Table_Internal<TFeatures, Person>") because constructTable returns a
Table<TFeatures, Person> that is not assignable to Table_Internal<TFeatures,
Person>; fix by making the types compatible instead of escaping the checker:
either change constructTable (or its factory) to declare/return
Table_Internal<TFeatures, Person> if the internals are required in tests, or
change generateTestTable to return the public Table<TFeatures, Person> and
update tests to use only the public API; locate constructTable and
generateTestTable and reconcile their signatures/members (ensure required
internal methods/properties exist on the chosen type), remove the double-cast,
and re-run type checks to confirm no further mismatches.
- Line 86: The code unsafely casts table.options to any to set state; instead
make the update type-safe by updating the test typings or API: either change the
generateTestTable/createTestTable helper to accept a typed state parameter and
assign it through the helper, or declare a specific mutable options type (e.g.,
MutableTableOptions) and cast to that when assigning (use as unknown as
MutableTableOptions) so you don't disable TS checks entirely; if you must bypass
types, add a clear comment documenting the reason and the expected shape of
state and reference the symbols table.options and state in the helper to ensure
future reviewers know this is intentional.

---

Outside diff comments:
In `@packages/table-core/src/fns/filterFns.ts`:
- Around line 293-302: The filterFn_arrHas export is missing the autoRemove
behavior used by other array filters; update the exported filter
(filterFn_arrHas) to include an autoRemove property set to (val) =>
testFalsy(val) || !val?.length (matching
arrIncludes/arrIncludesAll/arrIncludesSome) so the filter is auto-removed when
the value is falsy or an empty array; import or reuse the testFalsy helper if
needed and ensure the exported shape matches the other array filter fns.

---

Nitpick comments:
In `@docs/guide/custom-features.md`:
- Around line 242-261: Add a brief inline comment above the assignTableAPIs call
explaining the internal naming convention: that keys prefixed with "table_"
(e.g., table_setDensity, table_toggleDensity) are registered as instance methods
without the prefix (e.g., table.setDensity, table.toggleDensity) by
assignTableAPIs; reference the assignTableAPIs invocation and the specific keys
(table_setDensity, table_toggleDensity) and mention that these map to
table.options handlers like table.options.onDensityChange so readers understand
the mapping.

In `@examples/angular/custom-plugin/src/app/density/density-feature.ts`:
- Around line 81-89: The table_setDensity implementation contains a redundant
safeUpdater wrapper that merely calls functionalUpdate(updater, old) and returns
it; simplify by passing the original updater directly to
table.options.onDensityChange instead of creating safeUpdater (update the
table_setDensity fn to call table.options.onDensityChange?.(updater)), removing
the unnecessary safeUpdater and its use of functionalUpdate in this path.

In `@examples/preact/custom-plugin/src/main.tsx`:
- Around line 77-118: The safeUpdater wrapper in densityPlugin ->
constructTableAPIs for table_setDensity is redundant: instead of creating const
safeUpdater that just computes functionalUpdate(updater, old) and returns it,
pass the provided updater directly into the table options handler; replace the
block in table_setDensity.fn so it calls
table.options.onDensityChange?.(updater) (preserving the Updater<DensityState>
type) rather than wrapping it in safeUpdater. This removes unnecessary
indirection while keeping behavior identical.

In `@packages/angular-table/src/helpers/createTableHook.ts`:
- Around line 470-477: The current casts around injectTable/injectAppTable hide
a TData=any leak from defaultTableOptions/CreateTableContextOptions; fix by
making defaultTableOptions and CreateTableContextOptions generic over TData
(i.e., preserve TableOptions<TFeatures, TData> instead of defaulting to any) so
the constructed object already matches TableOptions<TFeatures, TData>, then
remove the intermediate "as unknown as TableOptions..." and final cast to
AppAngularTable; adjust the injectTable<>/injectAppTable<> call sites and the
signature of defaultTableOptions (and any helper that builds it) so
appTableFeatures.constructTableAPIs can still assign appCell/appHeader/appFooter
without needing unsafe casts.

In `@packages/table-core/src/core/table/constructTable.ts`:
- Around line 118-125: The non-null assertion on table.baseAtoms[key]!.get() in
constructTable assumes base atoms were created for every key in stateKeys; add a
runtime defensive check (only in development) before calling .get(): verify
table.baseAtoms[key] exists and throw a clear error or use
console.assert/invariant naming the missing key and referencing stateKeys and
the atom-creation loop (the code that builds baseAtoms) so refactors will
surface the invariant violation early; update the code path that reads
externalAtoms/externalAtom to use this check and keep behavior unchanged in
production.

In
`@packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts`:
- Line 42: The test is silencing TypeScript by casting the accessor options to
any in columnHelper.accessor(typedKey, { id: typedKey } as any); either remove
the as any cast and make typedKey satisfy the accessor's id type (adjust the
typedKey declaration or the generic/type parameter used by
columnHelper.accessor) or if the cast is truly unavoidable add a brief comment
explaining why the accessor's options typing cannot be satisfied here and
reference the specific constraint being bypassed; ensure the change is applied
at the call site using columnHelper.accessor and/or the typedKey definition so
the typing intent is preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b99d985-1813-42dc-9a48-ce877cfad5d8

📥 Commits

Reviewing files that changed from the base of the PR and between e42da9e and fd4421a.

📒 Files selected for processing (101)
  • docs/guide/custom-features.md
  • examples/angular/custom-plugin/src/app/density/density-feature.ts
  • examples/preact/custom-plugin/src/main.tsx
  • examples/react/custom-plugin/src/main.tsx
  • examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-column-header.tsx
  • examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-filter-list.tsx
  • examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-pagination.tsx
  • examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-sort-list.tsx
  • examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-view-options.tsx
  • examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-column-header.tsx
  • examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-filter-list.tsx
  • examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-pagination.tsx
  • examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-sort-list.tsx
  • examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-view-options.tsx
  • examples/svelte/sorting/src/App.svelte
  • examples/svelte/sorting/src/Header.svelte
  • packages/angular-table-devtools/src/index.ts
  • packages/angular-table-devtools/src/injectTanStackTableDevtools.ts
  • packages/angular-table/src/helpers/cell.ts
  • packages/angular-table/src/helpers/createTableHook.ts
  • packages/angular-table/src/helpers/header.ts
  • packages/angular-table/src/helpers/table.ts
  • packages/angular-table/tests/injectTable.test.ts
  • packages/react-table-devtools/src/table-devtools.d.ts
  • packages/react-table/src/useLegacyTable.ts
  • packages/table-core/src/core/cells/coreCellsFeature.ts
  • packages/table-core/src/core/columns/coreColumnsFeature.ts
  • packages/table-core/src/core/headers/buildHeaderGroups.ts
  • packages/table-core/src/core/headers/coreHeadersFeature.ts
  • packages/table-core/src/core/headers/coreHeadersFeature.utils.ts
  • packages/table-core/src/core/row-models/coreRowModelsFeature.ts
  • packages/table-core/src/core/row-models/coreRowModelsFeature.types.ts
  • packages/table-core/src/core/row-models/createCoreRowModel.ts
  • packages/table-core/src/core/rows/coreRowsFeature.ts
  • packages/table-core/src/core/table/constructTable.ts
  • packages/table-core/src/core/table/coreTablesFeature.ts
  • packages/table-core/src/core/table/coreTablesFeature.utils.ts
  • packages/table-core/src/features/column-faceting/columnFacetingFeature.ts
  • packages/table-core/src/features/column-faceting/createFacetedMinMaxValues.ts
  • packages/table-core/src/features/column-faceting/createFacetedRowModel.ts
  • packages/table-core/src/features/column-faceting/createFacetedUniqueValues.ts
  • packages/table-core/src/features/column-filtering/columnFilteringFeature.ts
  • packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts
  • packages/table-core/src/features/column-filtering/createFilteredRowModel.ts
  • packages/table-core/src/features/column-grouping/columnGroupingFeature.ts
  • packages/table-core/src/features/column-grouping/createGroupedRowModel.ts
  • packages/table-core/src/features/column-ordering/columnOrderingFeature.ts
  • packages/table-core/src/features/column-pinning/columnPinningFeature.ts
  • packages/table-core/src/features/column-pinning/columnPinningFeature.utils.ts
  • packages/table-core/src/features/column-resizing/columnResizingFeature.ts
  • packages/table-core/src/features/column-sizing/columnSizingFeature.ts
  • packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts
  • packages/table-core/src/features/global-filtering/globalFilteringFeature.ts
  • packages/table-core/src/features/row-expanding/createExpandedRowModel.ts
  • packages/table-core/src/features/row-expanding/rowExpandingFeature.ts
  • packages/table-core/src/features/row-pagination/createPaginatedRowModel.ts
  • packages/table-core/src/features/row-pagination/rowPaginationFeature.ts
  • packages/table-core/src/features/row-pinning/rowPinningFeature.ts
  • packages/table-core/src/features/row-selection/rowSelectionFeature.ts
  • packages/table-core/src/features/row-sorting/createSortedRowModel.ts
  • packages/table-core/src/features/row-sorting/rowSortingFeature.ts
  • packages/table-core/src/fns/aggregationFns.ts
  • packages/table-core/src/fns/filterFns.ts
  • packages/table-core/src/fns/sortFns.ts
  • packages/table-core/src/types/Cell.ts
  • packages/table-core/src/types/Column.ts
  • packages/table-core/src/types/ColumnDef.ts
  • packages/table-core/src/types/Header.ts
  • packages/table-core/src/types/HeaderGroup.ts
  • packages/table-core/src/types/Row.ts
  • packages/table-core/src/types/RowModel.ts
  • packages/table-core/src/types/RowModelFns.ts
  • packages/table-core/src/types/Table.ts
  • packages/table-core/src/types/TableFeatures.ts
  • packages/table-core/src/types/TableOptions.ts
  • packages/table-core/src/types/TableState.ts
  • packages/table-core/src/utils.ts
  • packages/table-core/tests/helpers/generateTestTable.ts
  • packages/table-core/tests/implementation/features/row-pinning/rowPinningFeature.test.ts
  • packages/table-core/tests/implementation/features/row-selection/rowSelectionFeature.test.ts
  • packages/table-core/tests/performance/features/column-grouping/columnGroupingFeature.test.ts
  • packages/table-core/tests/unit/core/cells/constructCell.test.ts
  • packages/table-core/tests/unit/core/columns/constructColumn.test.ts
  • packages/table-core/tests/unit/core/headers/constructHeader.test.ts
  • packages/table-core/tests/unit/core/rows/constructRow.test.ts
  • packages/table-core/tests/unit/core/table/stockFeaturesInitialState.test.ts
  • packages/table-core/tests/unit/core/tableAtoms.test.ts
  • packages/table-core/tests/unit/features/column-ordering/columnOrderingFeature.utils.test.ts
  • packages/table-core/tests/unit/features/column-pinning/columnPinningFeature.utils.test.ts
  • packages/table-core/tests/unit/features/column-resizing/columnResizingFeature.utils.test.ts
  • packages/table-core/tests/unit/features/column-visibility/columnVisibilityFeature.utils.test.ts
  • packages/table-core/tests/unit/features/row-pinning/rowPinningFeature.utils.test.ts
  • packages/table-devtools/src/TableContextProvider.tsx
  • packages/table-devtools/src/components/ColumnsPanel.tsx
  • packages/table-devtools/src/components/FeaturesPanel.tsx
  • packages/table-devtools/src/components/OptionsPanel.tsx
  • packages/table-devtools/src/index.ts
  • packages/table-devtools/src/production.ts
  • packages/table-devtools/src/tableTarget.ts
  • packages/vue-table/src/FlexRender.ts
  • packages/vue-table/src/createTableHook.ts

Comment on lines +93 to +103
header_getStart: {
fn: (header) => header_getStart(header),
memoDeps: (header, position) => [
position,
table.options.columns,
table.atoms.columnSizing?.get(),
table.atoms.columnOrder?.get(),
table.atoms.columnPinning?.get(),
table.atoms.columnVisibility?.get(),
],
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the signature of header_getStart to determine if it accepts a position parameter
ast-grep --pattern 'function header_getStart($_) { $$$ }'
ast-grep --pattern 'export function header_getStart($_) { $$$ }'
rg -nA5 'export function header_getStart' --type ts

Repository: TanStack/table

Length of output: 749


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/table-core/src/features/column-sizing/columnSizingFeature.ts"

# Show the header_getStart registration block with a bit more context
sed -n '70,130p' "$FILE" | nl -ba

# Show the column_getStart registration block for comparison (as referenced in the original comment)
sed -n '35,80p' "$FILE" | nl -ba

# Show the types/definition of the expected shape (where memoDeps/ fn signatures are declared), if present
rg -n "memoDeps|fn: \\(" "$FILE" --context 2

# Confirm whether any other header_getStart overload exists (position-less only, vs additional signatures)
rg -n "header_getStart\\b" packages/table-core/src/features/column-sizing --type ts

Repository: TanStack/table

Length of output: 101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/table-core/src/features/column-sizing/columnSizingFeature.ts"

echo "=== columnSizingFeature.ts (lines ~85-115) ==="
awk 'NR>=85 && NR<=115 {printf "%5d:%s\n", NR, $0}' "$FILE"

echo "=== columnSizingFeature.ts (lines ~40-85) ==="
awk 'NR>=40 && NR<=85 {printf "%5d:%s\n", NR, $0}' "$FILE"

echo "=== Find header_getStart registration occurrences ==="
rg -n "header_getStart" "$FILE"

echo "=== Locate type(s) that define memoDeps/registration function signatures ==="
# Search nearby for where the object shape is typed (best-effort)
rg -n "memoDeps" "$FILE" --context 3
rg -n "fn:" "$FILE" --context 3

Repository: TanStack/table

Length of output: 8593


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where assignPrototypeAPIs is implemented/typed
rg -n "assignPrototypeAPIs\\(" packages/table-core/src --type ts

# 2) Show the definition (first match) of assignPrototypeAPIs
DEF_FILE="$(rg -n "function assignPrototypeAPIs\\b|const assignPrototypeAPIs\\b|export function assignPrototypeAPIs\\b" packages/table-core/src --type ts -l | head -n 1 || true)"
echo "DEF_FILE=$DEF_FILE"
if [ -n "${DEF_FILE}" ]; then
  awk 'NR>=1 && NR<=220 {print NR":"$0}' "$DEF_FILE" | head -n 220
fi

# 3) Inspect header_getStart implementation details
UTIL_FILE="packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts"
echo "=== header_getStart implementation (around line 215-270) ==="
awk 'NR>=210 && NR<=280 {printf "%5d:%s\n", NR, $0}' "$UTIL_FILE"

Repository: TanStack/table

Length of output: 12419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find definition of assignPrototypeAPIs
rg -n "function assignPrototypeAPIs\\b|const assignPrototypeAPIs\\b|export function assignPrototypeAPIs\\b" packages/table-core/src --type ts

# Show surrounding implementation for first match
FILE="$(rg -n "function assignPrototypeAPIs\\b|const assignPrototypeAPIs\\b|export function assignPrototypeAPIs\\b" packages/table-core/src --type ts -l | head -n 1 || true)"
echo "DEF: $FILE"
if [ -n "${FILE}" ]; then
  awk 'NR>=1 && NR<=260 {print NR":"$0}' "$FILE" | head -n 260
fi

# Locate callMemoOrStaticFn which is used by header_getStart
rg -n "callMemoOrStaticFn\\b" packages/table-core/src --type ts -S | head -n 20
CALL_FILE="$(rg -n "callMemoOrStaticFn\\b" packages/table-core/src --type ts -l | head -n 1 || true)"
echo "CALL_FILE=$CALL_FILE"
if [ -n "${CALL_FILE}" ]; then
  awk 'NR>=1 && NR<=260 {print NR":"$0}' "$CALL_FILE" | head -n 260
fi

# Find usage of header_getStart/getStart with a position arg to confirm runtime deps
rg -n "header_getStart\\(" packages/table-core/src --type ts
rg -n "getStart\\b" packages/table-core/src/features packages/table-core/src/core --type ts | head -n 50

Repository: TanStack/table

Length of output: 20843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

UTIL="packages/table-core/src/utils.ts"
# Print assignPrototypeAPIs section around where it was found (line ~382)
awk 'NR>=360 && NR<=520 {printf "%5d:%s\n", NR, $0}' "$UTIL"

# Print callMemoOrStaticFn around where it was found (~428)
awk 'NR>=410 && NR<=500 {printf "%5d:%s\n", NR, $0}' "$UTIL"

TYPES_FILE="packages/table-core/src/features/column-sizing/columnSizingFeature.types.ts"
echo "=== columnSizingFeature.types.ts (relevant getStart typing) ==="
rg -n "header_getStart|getStart\\b" "$TYPES_FILE" --context 2
awk 'NR>=60 && NR<=120 {printf "%5d:%s\n", NR, $0}' "$TYPES_FILE"

FEATURE_TS="packages/table-core/src/features/column-sizing/columnSizingFeature.ts"
echo "=== columnSizingFeature.ts (header_getStart registration) ==="
awk 'NR>=80 && NR<=110 {printf "%5d:%s\n", NR, $0}' "$FEATURE_TS"

Repository: TanStack/table

Length of output: 7554


Remove redundant position from header_getStart memo deps

In packages/table-core/src/features/column-sizing/columnSizingFeature.ts (lines 93-103), header_getStart is registered with fn: (header) => header_getStart(header) but memoDeps includes position (memoDeps: (header, position) => [position, ...]). The header_getStart utility only accepts header (and doesn’t propagate position to sibling headers), so position doesn’t affect the computed result—only memo cache invalidation. Drop position from memoDeps or update header_getStart to accept/propagate position if header offsets should vary with pinning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/src/features/column-sizing/columnSizingFeature.ts` around
lines 93 - 103, The memoization dependencies for the registered selector
header_getStart include an unnecessary position argument; update the
registration in columnSizingFeature so memoDeps for header_getStart only depends
on header-related atoms (remove position from memoDeps array) OR alternatively,
if header offsets should vary by position/pinning, update the header_getStart
utility to accept and propagate position and then keep position in
memoDeps—refer to the header_getStart registration and the header_getStart
function to implement the chosen fix.

Comment on lines +127 to +147
export const filterFn_greaterThan = Object.assign(
<TFeatures extends TableFeatures, TData extends RowData>(
row: Row<TFeatures, TData>,
columnId: string,
filterValue: unknown,
) => {
const rowValue = row.getValue(columnId)
const numericRowValue =
rowValue === null || rowValue === undefined ? 0 : +rowValue
const numericFilterValue = Number(filterValue)

if (!isNaN(numericFilterValue) && !isNaN(numericRowValue)) {
return numericRowValue > numericFilterValue
}

const stringValue = (rowValue ?? '').toString().toLowerCase().trim()
const stringFilterValue = String(filterValue).toLowerCase().trim()
return stringValue > stringFilterValue
},
{ resolveFilterValue: (val: any) => testFalsy(val) },
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

resolveFilterValue should be autoRemove for comparison filters.

The resolveFilterValue hook is meant to transform the filter value before comparison (see filterFn_inNumberRange which returns [min, max]). Here it's set to testFalsy which returns a boolean, but comparison filters don't need value transformation—they need auto-removal of empty values.

The same issue appears in filterFn_greaterThanOrEqualTo (line 165), filterFn_lessThan (line 181), and filterFn_lessThanOrEqualTo (line 197).

🐛 Proposed fix
 export const filterFn_greaterThan = Object.assign(
   <TFeatures extends TableFeatures, TData extends RowData>(
     row: Row<TFeatures, TData>,
     columnId: string,
     filterValue: unknown,
   ) => {
     const rowValue = row.getValue(columnId)
     const numericRowValue =
       rowValue === null || rowValue === undefined ? 0 : +rowValue
     const numericFilterValue = Number(filterValue)

     if (!isNaN(numericFilterValue) && !isNaN(numericRowValue)) {
       return numericRowValue > numericFilterValue
     }

     const stringValue = (rowValue ?? '').toString().toLowerCase().trim()
     const stringFilterValue = String(filterValue).toLowerCase().trim()
     return stringValue > stringFilterValue
   },
-  { resolveFilterValue: (val: any) => testFalsy(val) },
+  { autoRemove: (val: any) => testFalsy(val) },
 )

Apply the same change to filterFn_greaterThanOrEqualTo, filterFn_lessThan, and filterFn_lessThanOrEqualTo.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const filterFn_greaterThan = Object.assign(
<TFeatures extends TableFeatures, TData extends RowData>(
row: Row<TFeatures, TData>,
columnId: string,
filterValue: unknown,
) => {
const rowValue = row.getValue(columnId)
const numericRowValue =
rowValue === null || rowValue === undefined ? 0 : +rowValue
const numericFilterValue = Number(filterValue)
if (!isNaN(numericFilterValue) && !isNaN(numericRowValue)) {
return numericRowValue > numericFilterValue
}
const stringValue = (rowValue ?? '').toString().toLowerCase().trim()
const stringFilterValue = String(filterValue).toLowerCase().trim()
return stringValue > stringFilterValue
},
{ resolveFilterValue: (val: any) => testFalsy(val) },
)
export const filterFn_greaterThan = Object.assign(
<TFeatures extends TableFeatures, TData extends RowData>(
row: Row<TFeatures, TData>,
columnId: string,
filterValue: unknown,
) => {
const rowValue = row.getValue(columnId)
const numericRowValue =
rowValue === null || rowValue === undefined ? 0 : +rowValue
const numericFilterValue = Number(filterValue)
if (!isNaN(numericFilterValue) && !isNaN(numericRowValue)) {
return numericRowValue > numericFilterValue
}
const stringValue = (rowValue ?? '').toString().toLowerCase().trim()
const stringFilterValue = String(filterValue).toLowerCase().trim()
return stringValue > stringFilterValue
},
{ autoRemove: (val: any) => testFalsy(val) },
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/src/fns/filterFns.ts` around lines 127 - 147, The
comparison filter fns are using resolveFilterValue incorrectly; replace the
object passed to Object.assign in filterFn_greaterThan,
filterFn_greaterThanOrEqualTo, filterFn_lessThan, and filterFn_lessThanOrEqualTo
so they use the autoRemove hook instead of resolveFilterValue — e.g. change {
resolveFilterValue: (val:any) => testFalsy(val) } to { autoRemove: testFalsy }
(refer to the symbols filterFn_greaterThan, filterFn_greaterThanOrEqualTo,
filterFn_lessThan, filterFn_lessThanOrEqualTo to locate each assignment).

coreReativityFeature: storeReactivityBindings(),
},
} as any)
} as any) as unknown as Table_Internal<TFeatures, Person>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Double casting suggests type misalignment between Table and Table_Internal.

The pattern as any) as unknown as Table_Internal<TFeatures, Person> is a complete type-system escape. This indicates that Table<TFeatures, Person> returned by constructTable is not compatible with Table_Internal<TFeatures, Person>, forcing the cast chain. While this unblocks the tests, it masks the underlying type incompatibility and could hide real type errors.

Consider whether Table_Internal should be the direct return type from constructTable, or whether test helpers should work with the public Table type instead.

Also applies to: 54-54

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/tests/helpers/generateTestTable.ts` at line 37, The test
helper is using a double-cast ("as any) as unknown as Table_Internal<TFeatures,
Person>") because constructTable returns a Table<TFeatures, Person> that is not
assignable to Table_Internal<TFeatures, Person>; fix by making the types
compatible instead of escaping the checker: either change constructTable (or its
factory) to declare/return Table_Internal<TFeatures, Person> if the internals
are required in tests, or change generateTestTable to return the public
Table<TFeatures, Person> and update tests to use only the public API; locate
constructTable and generateTestTable and reconcile their signatures/members
(ensure required internal methods/properties exist on the chosen type), remove
the double-cast, and re-run type checks to confirm no further mismatches.

}

table.options.state = state
;(table.options as any).state = state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unsafe state mutation bypasses type safety.

Casting table.options to any before assigning state completely disables TypeScript checking. If options.state is readonly or has a different shape, this will silently break at runtime.

🔒 Consider using a type-safe state update approach

If table.options.state is intentionally mutable for tests, declare it as such in the test typing. If it's meant to be readonly, use proper reactivity APIs instead of bypassing the type system:

-      ;(table.options as any).state = state
+      // If options.state is meant to be mutable for test purposes:
+      table.options.state = state
+      // Or use proper table API if available

If the assignment legitimately requires an any cast, document why with a comment explaining the type constraint being worked around.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/table-core/tests/helpers/generateTestTable.ts` at line 86, The code
unsafely casts table.options to any to set state; instead make the update
type-safe by updating the test typings or API: either change the
generateTestTable/createTestTable helper to accept a typed state parameter and
assign it through the helper, or declare a specific mutable options type (e.g.,
MutableTableOptions) and cast to that when assigning (use as unknown as
MutableTableOptions) so you don't disable TS checks entirely; if you must bypass
types, add a clear comment documenting the reason and the expected shape of
state and reference the symbols table.options and state in the helper to ensure
future reviewers know this is intentional.

@KevinVandy KevinVandy merged commit 37d7a10 into beta Jun 8, 2026
8 checks passed
@KevinVandy KevinVandy deleted the plugin-better branch June 8, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant