Skip to content

feat(table): support external reactivity binding#6237

Draft
riccardoperra wants to merge 11 commits intoalphafrom
feat/native-reactivity
Draft

feat(table): support external reactivity binding#6237
riccardoperra wants to merge 11 commits intoalphafrom
feat/native-reactivity

Conversation

@riccardoperra
Copy link
Copy Markdown
Collaborator

@riccardoperra riccardoperra commented Apr 22, 2026

Allows constructTable to support a new reactivity fields used by adapters to pass custom signals implementation

Summary by CodeRabbit

  • Dependencies Updated

    • Routine minor version updates across example projects and core packages
    • Updated validation libraries and development tooling
  • Improvements

    • Modernized table reactivity architecture for better framework integration
    • Enhanced Angular table adapter with native signal support
    • Improved reactivity consistency across React, Vue, Solid, Svelte, and Preact adapters for more efficient updates

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

This PR refactors TanStack Table's reactivity system from a framework-specific feature-based approach to a framework-agnostic TableReactivityBindings abstraction. The table-core layer now requires explicit reactivity bindings; framework adapters (React, Vue, Solid, Svelte, Angular, Preact, Lit) each provide their own implementations. Test fixtures and minor dependency versions are updated accordingly.

Changes

Reactivity Abstraction & Core Migration

Layer / File(s) Summary
Reactivity Types & Adapters
packages/table-core/src/features/table-reactivity/table-reactivity.ts, tanstack-signals.ts
Introduces TableReactivityBindings, TableAtomOptions, and atomToStore adapter utilities. tanstackSignals() provides a signals-based binding backed by @tanstack/store.
Core Table Refactor
packages/table-core/src/core/table/constructTable.ts, coreTablesFeature.types.ts, coreTablesFeature.utils.ts
constructTable now requires reactivity: TableReactivityBindings in options. State management switches from direct store APIs to atom-based bindings via signals interface. optionsStore becomes a writable atom; individual state atoms are created and derived using the provided reactivity bindings.
Removed Legacy Feature
packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts
Deleted constructReactivityFeature and its notifier-wrapping infrastructure, which are no longer needed with the new direct binding approach.
Export Updates
packages/table-core/package.json, src/index.ts, tsdown.config.ts
Package exports updated to include ./features/table-reactivity/tanstack-signals subpath; public exports now reference the new table-reactivity module instead of the deleted tableReactivityFeature.
Test Fixtures
packages/table-core/tests/helpers/generateTestTable.ts, rowPinningHelpers.ts, tests/implementation/..., tests/unit/...
All test table generators and unit tests now explicitly pass reactivity: tanstackSignals() to constructTable to initialize tables with signals-based reactivity.

Framework Adapters: React, Vue, Solid, Svelte, Angular, Preact, Lit

Layer / File(s) Summary
React Adapter
packages/react-table/src/useTable.ts
useTable injects reactivity: tanstackSignals() into table options; moved option syncing from synchronous render-time call to useEffect; derives options from table.optionsStore via useSelector instead of returning tableOptions directly.
Vue Adapter
packages/vue-table/src/signals.ts, src/useTable.ts
New vueReactivity() function bridges Vue reactivity (refs, computed) to TableReactivityBindings. useTable now injects reactivity: tableOptions.reactivity ?? vueReactivity(); removed notifier/watchEffect in favor of explicit watch on reactive option dependencies and controlled state slices.
Solid Adapter
packages/solid-table/src/signals.ts, src/createTable.ts
New solidReactivity(owner) provides Solid-specific reactivity bindings. createTable obtains owner via getOwner(), injects reactivity: solidReactivity(owner), and removes prior constructReactivityFeature/notifier mechanism.
Svelte Adapter
packages/svelte-table/src/signals.svelte.ts, src/createTable.svelte.ts
New svelteReactivity() bridges Svelte runes ($state, $derived.by) to reactivity bindings. createTable injects reactivity: svelteReactivity(); replaced notifier-driven store subscription with a single $effect.pre tracking state/data dependencies and calling table.setOptions.
Angular Adapter
packages/angular-table/src/signals.ts, src/injectTable.ts
New angularReactivity(injector) converts Angular signals to reactivity bindings. injectTable removes constructReactivityFeature/injectSelector wiring; now constructs table with reactivity: angularReactivity(injector) and updates options via an effect guard that calls table.setOptions on reactive option changes. AngularTable.computed signature narrowed; Subscribe member removed.
Preact Adapter
packages/preact-table/src/signals.ts, src/useTable.ts, package.json
New preactReactivity() wraps Preact signals as reactivity bindings. useTable injects reactivity: preactReactivity() into table construction. Added @preact/signals dependency.
Lit Adapter
packages/lit-table/src/TableController.ts, tests/unit/defaultReactivity.test.ts
TableController.table() now injects reactivity: tanstackSignals() into merged options instead of using litReactivityFeature. Added unit test verifying table.reactivity is defined on constructed tables.
Angular Build Config
packages/angular-table/vite.config.ts, package.json
Removed jit: true from Angular Vite plugin configuration; removed test:benchmark script from package.json. Benchmark files deleted; integration test re-enabled with updated reactivity expectations.

Dependency Updates

Layer / File(s) Summary
Example Dependencies
examples/react/mantine-react-table/package.json, examples/react/with-tanstack-form/package.json, examples/react/with-tanstack-router/package.json, examples/solid/with-tanstack-form/package.json, examples/solid/with-tanstack-router/package.json, examples/svelte/with-tanstack-form/package.json, examples/vue/with-tanstack-form/package.json
Minor version bumps: dayjs (^1.11.13 → ^1.11.20), zod (^4.4.1 → ^4.4.2 across multiple examples), @tanstack/router-vite-plugin (^1.166.46 → ^1.166.47).
Package Dependencies
packages/react-table-devtools/package.json, packages/react-table/package.json
@eslint-react/eslint-plugin bumped from ^5.6.6 to ^5.7.0.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 The signals now flow through bindings anew,
Framework adapters each have their true
Reactivity dance—from Vue to Angular's spree,
The table core sings with atoms so free!
No more notifier tricks, just pure binding bliss,
TanStack tables evolved—don't miss this! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(table): support external reactivity binding' accurately summarizes the main change in the PR, which adds support for external reactivity bindings to the table system via a new reactivity field in constructTable.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/native-reactivity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 22, 2026

View your CI Pipeline Execution ↗ for commit 4984c5c

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

☁️ Nx Cloud last updated this comment at 2026-05-02 02:20:47 UTC

@riccardoperra riccardoperra force-pushed the feat/native-reactivity branch from 5d74b34 to 20e26ec Compare April 22, 2026 20:26
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 22, 2026

More templates

@tanstack/angular-table

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

@tanstack/lit-table

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

@tanstack/match-sorter-utils

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

@tanstack/preact-table

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

@tanstack/preact-table-devtools

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

@tanstack/react-table

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

@tanstack/react-table-devtools

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

@tanstack/solid-table

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

@tanstack/solid-table-devtools

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

@tanstack/svelte-table

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

@tanstack/table-core

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

@tanstack/table-devtools

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

@tanstack/vue-table

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

@tanstack/vue-table-devtools

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

commit: 4984c5c

@riccardoperra riccardoperra force-pushed the feat/native-reactivity branch 2 times, most recently from e54fed5 to fc27ad3 Compare April 27, 2026 18:06
@riccardoperra riccardoperra force-pushed the feat/native-reactivity branch from fc27ad3 to 5a5be8b Compare April 28, 2026 17:21
Copy link
Copy Markdown

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

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

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud is proposing a fix for your failed CI:

We add the missing import { solidReactivity } from './signals' to packages/solid-table/src/createTable.ts, which fixes the Cannot find name 'solidReactivity' TypeScript error introduced when the PR refactored native reactivity bindings into a separate signals.ts file. This also resolves the knip "unused file" warning for signals.ts, as the file is now reachable from createTable.ts.

Tip

We verified this fix by re-running @tanstack/solid-table:test:types, table:test:knip.

diff --git a/packages/solid-table/src/createTable.ts b/packages/solid-table/src/createTable.ts
index 09790fc79..fe62fde30 100644
--- a/packages/solid-table/src/createTable.ts
+++ b/packages/solid-table/src/createTable.ts
@@ -2,6 +2,7 @@ import { constructTable } from '@tanstack/table-core'
 import { createComputed, getOwner, mergeProps, untrack } from 'solid-js'
 import { shallow, useSelector } from '@tanstack/solid-store'
 import { FlexRender } from './FlexRender'
+import { solidReactivity } from './signals'
 import type { Atom, ReadonlyAtom } from '@tanstack/solid-store'
 import type { Accessor, JSX } from 'solid-js'
 import type {

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally lZFc-mprV

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

@KevinVandy
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (2)
packages/table-core/src/core/table/coreTablesFeature.utils.ts (1)

11-16: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

table_reset() no longer resets externally bound state slices.

After this PR, constructTable() resolves each slice from options.atoms?.[key] before falling back to baseAtoms[key], but table_reset() still only writes baseAtoms. In the new native-reactivity mode, table.reset() becomes a no-op for any slice backed by an external atom because the visible source of truth never changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/src/core/table/coreTablesFeature.utils.ts` around lines
11 - 16, table_reset() currently writes only to baseAtoms so slices bound to
external atoms via options.atoms are not reset; update the reset logic to
resolve the actual atom for each key (the same resolution constructTable uses:
options.atoms?.[key] fallback to baseAtoms[key]) and write the snap value into
that resolved atom. Concretely, in the reset implementation that uses
cloneState(table.initialState) and table.reactivity.batch(), iterate keys and
call set on the resolved atom map (e.g., the per-slice atoms object created
during constructTable or a helper that resolves options.atoms?.[key] ||
baseAtoms[key]) instead of always using (table.baseAtoms as any)[key]. Ensure
you reference the same resolution logic used by constructTable so external atoms
observe the reset.
packages/svelte-table/src/createTable.svelte.ts (1)

72-80: ⚠️ Potential issue | 🟠 Major

Add columns to the reactive dependencies tracked by this effect.

This effect only tracks changes to state and data. In Svelte 5, an effect only reruns when properties that are synchronously read inside its function body change. If columns is reactive (passed as a getter or signal), updating it will not rerun this effect, leaving the table with stale column definitions.

Suggested fix
     if (state) {
       for (const key in state) {
         void state[key]
       }
     }
     void mergedOptions.data
+    void mergedOptions.columns
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/svelte-table/src/createTable.svelte.ts` around lines 72 - 80, The
effect currently only reads mergedOptions.state and mergedOptions.data so it
won't retrigger when reactive columns change; update the $effect.pre callback to
also synchronously read mergedOptions.columns (e.g., assign or void
mergedOptions.columns or iterate over it if it's an object/array) so Svelte 5
will track columns as a dependency and rerun the effect when columns updates
occur; locate the $effect.pre block in createTable.svelte.ts and add a
synchronous read of mergedOptions.columns (use void mergedOptions.columns or a
safe iteration if it may be undefined) alongside the existing reads of state and
data.
🧹 Nitpick comments (6)
packages/vue-table/tests/unit/signals.test.ts (1)

1-22: ⚡ Quick win

Consider adding a test that exercises the subscribe callback path.

The current test only verifies get() on writable and readonly atoms. The subscribe / unsubscribe contract — including that watch({ flush: 'sync' }) notifications fire synchronously and that unsubscribe() actually stops further callbacks — is untested. Given that the subscription mechanism is the main reactivity bridge between Vue's scheduler and table-core, a subscription-side test would meaningfully increase confidence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-table/tests/unit/signals.test.ts` around lines 1 - 22, Add a
unit test that exercises the subscription path of the vueReactivity bridge:
instantiate vueReactivity(), create a writable atom via createWritableAtom(...)
and a readonly atom via createReadonlyAtom(...), then call subscribe(callback)
on the readonly atom (or writable) and assert the callback is called
synchronously when you update the writable atom (use Vue's watch with { flush:
'sync' } behavior expectation) and that unsubscribe() returned by subscribe
stops further callbacks; reference createWritableAtom, createReadonlyAtom,
subscribe/unsubscribe and ensure you await/drive a nextTick only where
appropriate to distinguish sync vs async notifications.
packages/angular-table/src/signals.ts (1)

11-42: ⚡ Quick win

Redundant computed(signal) wrapper in subscribe — use toObservable(signal, { injector }) directly.

Both signalToReadonlyAtom and signalToWritableAtom wrap the already-reactive signal in an extra computed(signal) before calling toObservable. Since signal is already a Signal<T> (and WritableSignal<T> extends it), toObservable can accept it directly. The intermediate computed is created fresh on every subscribe() call, allocating an unnecessary reactive node each time.

♻️ Proposed cleanup
 function signalToReadonlyAtom<T>(
   signal: Signal<T>,
   injector: Injector,
 ): ReadonlyAtom<T> {
   return Object.assign(signal, {
     get: () => signal(),
     subscribe: (observer: Observer<T>) => {
-      return toObservable(computed(signal), { injector: injector }).subscribe(
-        observer,
-      )
+      return toObservable(signal, { injector }).subscribe(observer)
     },
   })
 }

 function signalToWritableAtom<T>(
   signal: WritableSignal<T>,
   injector: Injector,
 ): Atom<T> {
   return Object.assign(signal.asReadonly(), {
     set: ...,
     get: () => signal(),
     subscribe: (observer: Observer<T>) => {
-      return toObservable(computed(signal), { injector: injector }).subscribe(
-        observer,
-      )
+      return toObservable(signal, { injector }).subscribe(observer)
     },
   })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-table/src/signals.ts` around lines 11 - 42, The subscribe
implementations in signalToReadonlyAtom and signalToWritableAtom create an extra
computed(signal) reactive node per subscription; change both subscribe callbacks
to call toObservable(signal, { injector }) directly (i.e., replace
toObservable(computed(signal), { injector }) with toObservable(signal, {
injector })) so the existing Signal/WritableSignal is used without allocating a
new computed each subscribe.
packages/preact-table/tests/unit/signals.test.ts (1)

1-20: ⚡ Quick win

Missing subscribe test — would have caught the infinite-recursion bug in signals.ts.

The suite only exercises .get() and .set(fn). Adding a subscribe test would directly expose the stack-overflow introduced by Object.assign mutating source.subscribe before the wrapper closes over it.

💡 Suggested additional test
+  test('subscribe emits current and future values', () => {
+    const reactivity = preactReactivity()
+    const count = reactivity.createWritableAtom(0)
+    const received: number[] = []
+    const sub = count.subscribe((v) => received.push(v))
+
+    count.set(1)
+    count.set(2)
+
+    sub.unsubscribe()
+    count.set(3)
+
+    expect(received).toContain(1)
+    expect(received).toContain(2)
+    expect(received).not.toContain(3)
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/preact-table/tests/unit/signals.test.ts` around lines 1 - 20, Add a
unit test that calls the atom's subscribe path to reproduce the
infinite-recursion bug: in tests for preactReactivity(), create a writable atom
via createWritableAtom and subscribe to it (and unsubscribe) asserting the
callback is invoked on set; this will expose the stack overflow caused by
mutating source.subscribe. Then fix signals.ts by stopping the in-place mutation
via Object.assign of the original source.subscribe; instead return or attach a
new wrapper subscribe function (do not overwrite source.subscribe) so the
wrapper closes over the original subscribe reference without reassigning it
(look for the Object.assign usage and the subscribe wrapper in the
createWritableAtom/createReadonlyAtom implementation).
packages/solid-table/src/signals.ts (1)

22-24: ⚡ Quick win

Non-null assertion on runWithOwner result hides a potential undefined.

runWithOwner<T>(owner, fn) can return T | undefined (e.g. if fn throws). The ! silently converts a potential undefined to a runtime crash at .subscribe(observer). While owner is guaranteed non-null at the call site, a throw inside observable(signal) would still surface as an unhandled error rather than a clean message.

💡 Safer alternative
-      return runWithOwner(owner, () => observable(signal))!.subscribe(observer)
+      const obs = runWithOwner(owner, () => observable(signal))
+      if (!obs) throw new Error('[solid-table] runWithOwner returned undefined — owner may be disposed')
+      return obs.subscribe(observer)

Also applies to: 40-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-table/src/signals.ts` around lines 22 - 24, Replace the
non-null assertion on runWithOwner(...)!.subscribe with a safe pattern: call
const obs = runWithOwner(owner, () => observable(signal)) inside a try/catch, if
obs is undefined throw or rethrow a clear Error like "runWithOwner returned
undefined while creating observable" (or rethrow the caught error with added
context), then call obs.subscribe(observer); do the same for the other
occurrences (the subscribe implementation and the spots at lines ~40-42) so we
never call .subscribe on a possibly undefined value.
packages/table-core/tests/unit/core/table/constructTable.test.ts (1)

7-14: ⚡ Quick win

Add a regression test for atom-backed state, not just construction.

This still only proves that constructTable(...) accepts a reactivity implementation. The new behavior is the external atoms/state sourcing path, so a focused test that binds one slice through an external atom and asserts table.atoms[key] plus table.reset() behavior would catch regressions in this change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/tests/unit/core/table/constructTable.test.ts` around
lines 7 - 14, The test only verifies constructTable accepts a reactivity
implementation; add a unit test that exercises the external atom-backed state
path by creating an external atom or state slice, passing it into constructTable
(using the same reactivity e.g. tanstackSignals) so the table uses that external
source, then assert that table.atoms[<key>] (or table.state[<key>]) references
the provided atom and that invoking table.reset() restores the atom-backed slice
to its initial value; target helpers and symbols: constructTable, table.atoms
(or table.state), and table.reset to locate the relevant API in the test file
and assert both reference equality and reset behavior to catch regressions.
packages/lit-table/src/TableController.ts (1)

136-136: 💤 Low value

Unused _notifier field.

The _notifier field is incremented in the subscription callbacks but never read. If it's not needed for debugging or future use, consider removing it to reduce noise.

♻️ Proposed removal
   private _table: Table<TFeatures, TData> | null = null
   private _storeSubscription?: { unsubscribe: () => void }
   private _optionsSubscription?: { unsubscribe: () => void }
-  private _notifier = 0
     if (this._table && !this._storeSubscription) {
       this._storeSubscription = this._table.store.subscribe(() => {
-        this._notifier++
         this.host.requestUpdate()
       })

       this._optionsSubscription = this._table.optionsStore.subscribe(() => {
-        this._notifier++
         this.host.requestUpdate()
       })
     }

Also applies to: 213-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-table/src/TableController.ts` at line 136, Remove the unused
private field _notifier from the TableController class and delete all increments
to it in the subscription callbacks (where ++this._notifier is used); if the
increments were intended as a change-trigger, either replace them with a proper
state/readable signal or simply remove both the field declaration and those
increment statements to eliminate dead code and noise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/angular-table/tests/angularReactivityFeature.test.ts`:
- Around line 40-41: The test "methods within effect will be re-trigger when
options/state changes" is flaky due to a race between state changes and
assertion timing; make it deterministic by synchronizing on the effect
completion (e.g., await the next microtask/tick or use a test utility like
waitFor/waitForNextUpdate) and stabilize call counting by resetting mocks before
the test and asserting after you run pending timers (jest.runAllTimers or await
act/nextTick) so the method-call count is consistently observed; update the test
to explicitly wait for the re-triggered effect and then assert the exact number
of calls rather than relying on timing-sensitive immediate assertions.

In `@packages/angular-table/tests/injectTable.test.ts`:
- Around line 127-128: The second duplicate assertion should check the second
invocation of the mock instead of repeating the first; update the duplicate
expect to reference coreRowModelFn.mock.calls[1] (the second call) and assert
its [0].rows.length equals 10 so the test verifies both calls to coreRowModelFn.

In `@packages/preact-table/src/signals.ts`:
- Around line 16-46: The subscribe wrapper currently shadows and replaces the
original Preact subscribe method causing infinite recursion; in both
signalToReadonlyAtom and signalToWritableAtom capture the original subscribe
into a local (e.g. const nativeSubscribe = source.subscribe) before calling
Object.assign, then implement the wrapper to call
nativeSubscribe(observerToCallback(...)) instead of source.subscribe(...); keep
the rest of the wrapper behavior (returning { unsubscribe }) and ensure get/set
remain unchanged.

In `@packages/react-table/src/useTable.ts`:
- Around line 142-147: The current useEffect block that calls
table.setOptions(({...prev, ...tableOptions})) causes post-commit timing and
one-render staleness for computed methods like getRowModel() and
getHeaderGroups(); change this to useLayoutEffect so options are synchronized
pre-paint (before the browser paints) and visible within the same render
pass—replace the useEffect that references table.setOptions, table, and
tableOptions with useLayoutEffect and keep the same merge logic to avoid
render-phase mutations.

In `@packages/solid-table/src/createTable.ts`:
- Around line 77-79: The merge order currently forces the default reactivity to
override caller-provided values; change the merge so the default comes first and
caller options last (swap the args to mergeProps) so caller-provided reactivity
wins: update the mergedOptions creation that calls mergeProps(tableOptions, {
reactivity: solidReactivity(owner) }) to call mergeProps({ reactivity:
solidReactivity(owner) }, tableOptions) instead, keeping the same symbols
mergedOptions, mergeProps, tableOptions, solidReactivity(owner), and the
reactivity property.

In `@packages/table-core/src/core/table/constructTable.ts`:
- Around line 21-32: The public API currently requires reactivity by changing
the ConstructTable type and constructTable parameter to include a mandatory
reactivity, which is a breaking change; revert this by making reactivity
optional on the public ConstructTable/constructTable signature (e.g., keep
TableOptions<TFeatures,TData> & { reactivity?: TableReactivityBindings }) and
inside constructTable (the function using tableOptions.reactivity) default to
the existing internal bindings when reactivity is undefined (create or assign
the default TableReactivityBindings before using signals). Ensure you reference
and update ConstructTable, constructTable, TableOptions, and
TableReactivityBindings so consumers without explicit reactivity continue to get
the previous behavior.

In `@packages/table-core/src/core/table/coreTablesFeature.types.ts`:
- Around line 112-115: Fix the JSDoc typo for the Table custom reactibity
bindings by changing "reactibity" to "reactivity" above the readonly
reactivity?: TableReactivityBindings property; update the comment text so it
reads "Table custom reactivity bindings." to match the property name and type
(TableReactivityBindings) and ensure consistency across any nearby docs/comments
referencing this symbol.

In `@packages/table-core/src/features/table-reactivity/table-reactivity.ts`:
- Around line 61-74: The function atomToStore mutates the incoming Atom (using
Object.defineProperty on atom and assigning store.setState), which can throw on
subsequent calls and corrupt the original Atom; change it to return a thin
wrapper object that inherits from the atom (e.g., wrapper = Object.create(atom))
so the original atom is not mutated, define the 'state' accessor on the wrapper
with configurable: true, and assign wrapper.setState (not atom or store
referencing the same object) to atom.set.bind(atom) when a setter exists; keep
all other atom methods available via the prototype chain so atomToStore remains
a pure adapter.

In `@packages/vue-table/src/signals.ts`:
- Around line 60-61: The current untrack implementation (untrack: (fn) => fn())
does not suppress Vue reactive tracking; replace it with a proper pause/reset
pair from Vue internals by importing pauseTracking and resetTracking from
'@vue/reactivity' and implement untrack to call pauseTracking(), invoke fn() in
a try block, and call resetTracking() in finally so tracking is always restored;
keep batch as-is or document its behavior if you choose not to change it.

---

Outside diff comments:
In `@packages/svelte-table/src/createTable.svelte.ts`:
- Around line 72-80: The effect currently only reads mergedOptions.state and
mergedOptions.data so it won't retrigger when reactive columns change; update
the $effect.pre callback to also synchronously read mergedOptions.columns (e.g.,
assign or void mergedOptions.columns or iterate over it if it's an object/array)
so Svelte 5 will track columns as a dependency and rerun the effect when columns
updates occur; locate the $effect.pre block in createTable.svelte.ts and add a
synchronous read of mergedOptions.columns (use void mergedOptions.columns or a
safe iteration if it may be undefined) alongside the existing reads of state and
data.

In `@packages/table-core/src/core/table/coreTablesFeature.utils.ts`:
- Around line 11-16: table_reset() currently writes only to baseAtoms so slices
bound to external atoms via options.atoms are not reset; update the reset logic
to resolve the actual atom for each key (the same resolution constructTable
uses: options.atoms?.[key] fallback to baseAtoms[key]) and write the snap value
into that resolved atom. Concretely, in the reset implementation that uses
cloneState(table.initialState) and table.reactivity.batch(), iterate keys and
call set on the resolved atom map (e.g., the per-slice atoms object created
during constructTable or a helper that resolves options.atoms?.[key] ||
baseAtoms[key]) instead of always using (table.baseAtoms as any)[key]. Ensure
you reference the same resolution logic used by constructTable so external atoms
observe the reset.

---

Nitpick comments:
In `@packages/angular-table/src/signals.ts`:
- Around line 11-42: The subscribe implementations in signalToReadonlyAtom and
signalToWritableAtom create an extra computed(signal) reactive node per
subscription; change both subscribe callbacks to call toObservable(signal, {
injector }) directly (i.e., replace toObservable(computed(signal), { injector })
with toObservable(signal, { injector })) so the existing Signal/WritableSignal
is used without allocating a new computed each subscribe.

In `@packages/lit-table/src/TableController.ts`:
- Line 136: Remove the unused private field _notifier from the TableController
class and delete all increments to it in the subscription callbacks (where
++this._notifier is used); if the increments were intended as a change-trigger,
either replace them with a proper state/readable signal or simply remove both
the field declaration and those increment statements to eliminate dead code and
noise.

In `@packages/preact-table/tests/unit/signals.test.ts`:
- Around line 1-20: Add a unit test that calls the atom's subscribe path to
reproduce the infinite-recursion bug: in tests for preactReactivity(), create a
writable atom via createWritableAtom and subscribe to it (and unsubscribe)
asserting the callback is invoked on set; this will expose the stack overflow
caused by mutating source.subscribe. Then fix signals.ts by stopping the
in-place mutation via Object.assign of the original source.subscribe; instead
return or attach a new wrapper subscribe function (do not overwrite
source.subscribe) so the wrapper closes over the original subscribe reference
without reassigning it (look for the Object.assign usage and the subscribe
wrapper in the createWritableAtom/createReadonlyAtom implementation).

In `@packages/solid-table/src/signals.ts`:
- Around line 22-24: Replace the non-null assertion on
runWithOwner(...)!.subscribe with a safe pattern: call const obs =
runWithOwner(owner, () => observable(signal)) inside a try/catch, if obs is
undefined throw or rethrow a clear Error like "runWithOwner returned undefined
while creating observable" (or rethrow the caught error with added context),
then call obs.subscribe(observer); do the same for the other occurrences (the
subscribe implementation and the spots at lines ~40-42) so we never call
.subscribe on a possibly undefined value.

In `@packages/table-core/tests/unit/core/table/constructTable.test.ts`:
- Around line 7-14: The test only verifies constructTable accepts a reactivity
implementation; add a unit test that exercises the external atom-backed state
path by creating an external atom or state slice, passing it into constructTable
(using the same reactivity e.g. tanstackSignals) so the table uses that external
source, then assert that table.atoms[<key>] (or table.state[<key>]) references
the provided atom and that invoking table.reset() restores the atom-backed slice
to its initial value; target helpers and symbols: constructTable, table.atoms
(or table.state), and table.reset to locate the relevant API in the test file
and assert both reference equality and reset behavior to catch regressions.

In `@packages/vue-table/tests/unit/signals.test.ts`:
- Around line 1-22: Add a unit test that exercises the subscription path of the
vueReactivity bridge: instantiate vueReactivity(), create a writable atom via
createWritableAtom(...) and a readonly atom via createReadonlyAtom(...), then
call subscribe(callback) on the readonly atom (or writable) and assert the
callback is called synchronously when you update the writable atom (use Vue's
watch with { flush: 'sync' } behavior expectation) and that unsubscribe()
returned by subscribe stops further callbacks; reference createWritableAtom,
createReadonlyAtom, subscribe/unsubscribe and ensure you await/drive a nextTick
only where appropriate to distinguish sync vs async notifications.
🪄 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: 4fb02d43-1779-4f51-8252-70d91fffdc82

📥 Commits

Reviewing files that changed from the base of the PR and between 29b71c3 and 4984c5c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (52)
  • examples/react/mantine-react-table/package.json
  • examples/react/with-tanstack-form/package.json
  • examples/react/with-tanstack-router/package.json
  • examples/solid/with-tanstack-form/package.json
  • examples/solid/with-tanstack-router/package.json
  • examples/svelte/with-tanstack-form/package.json
  • examples/vue/with-tanstack-form/package.json
  • packages/angular-table/package.json
  • packages/angular-table/src/injectTable.ts
  • packages/angular-table/src/lazySignalInitializer.ts
  • packages/angular-table/src/signals.ts
  • packages/angular-table/tests/angularReactivityFeature.test.ts
  • packages/angular-table/tests/benchmarks/injectTable.benchmark.ts
  • packages/angular-table/tests/benchmarks/setup.ts
  • packages/angular-table/tests/flex-render/flex-render-table.test.ts
  • packages/angular-table/tests/injectTable.test.ts
  • packages/angular-table/vite.config.ts
  • packages/lit-table/src/TableController.ts
  • packages/lit-table/tests/unit/defaultReactivity.test.ts
  • packages/preact-table/package.json
  • packages/preact-table/src/signals.ts
  • packages/preact-table/src/useTable.ts
  • packages/preact-table/tests/unit/signals.test.ts
  • packages/react-table-devtools/package.json
  • packages/react-table/package.json
  • packages/react-table/src/useTable.ts
  • packages/solid-table/src/createTable.ts
  • packages/solid-table/src/signals.ts
  • packages/svelte-table/src/createTable.svelte.ts
  • packages/svelte-table/src/signals.svelte.ts
  • packages/table-core/package.json
  • packages/table-core/src/core/table/constructTable.ts
  • packages/table-core/src/core/table/coreTablesFeature.types.ts
  • packages/table-core/src/core/table/coreTablesFeature.utils.ts
  • packages/table-core/src/features/table-reactivity/table-reactivity.ts
  • packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts
  • packages/table-core/src/features/table-reactivity/tanstack-signals.ts
  • packages/table-core/src/index.ts
  • packages/table-core/src/types/Table.ts
  • packages/table-core/tests/helpers/generateTestTable.ts
  • packages/table-core/tests/helpers/rowPinningHelpers.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/columns/constructColumn.test.ts
  • packages/table-core/tests/unit/core/table/constructTable.test.ts
  • packages/table-core/tests/unit/core/table/stockFeaturesInitialState.test.ts
  • packages/table-core/tests/unit/core/tableAtoms.test.ts
  • packages/table-core/tsdown.config.ts
  • packages/vue-table/src/signals.ts
  • packages/vue-table/src/useTable.ts
  • packages/vue-table/tests/unit/signals.test.ts
💤 Files with no reviewable changes (5)
  • packages/angular-table/tests/benchmarks/injectTable.benchmark.ts
  • packages/angular-table/package.json
  • packages/angular-table/tests/benchmarks/setup.ts
  • packages/angular-table/tests/flex-render/flex-render-table.test.ts
  • packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts

Comment on lines 40 to +41
// TODO this switches between 1 and 2 calls on every other run, so it's not a reliable test
test.skip('methods within effect will be re-trigger when options/state changes', () => {
test('methods within effect will be re-trigger when options/state changes', () => {
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 | ⚖️ Poor tradeoff

Address test flakiness before unskipping.

The TODO comment indicates this test "switches between 1 and 2 calls on every other run." Re-enabling a known flaky test may cause intermittent CI failures. Consider either resolving the underlying timing issue or adding a more robust synchronization mechanism before unskipping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-table/tests/angularReactivityFeature.test.ts` around lines
40 - 41, The test "methods within effect will be re-trigger when options/state
changes" is flaky due to a race between state changes and assertion timing; make
it deterministic by synchronizing on the effect completion (e.g., await the next
microtask/tick or use a test utility like waitFor/waitForNextUpdate) and
stabilize call counting by resetting mocks before the test and asserting after
you run pending timers (jest.runAllTimers or await act/nextTick) so the
method-call count is consistently observed; update the test to explicitly wait
for the re-triggered effect and then assert the exact number of calls rather
than relying on timing-sensitive immediate assertions.

Comment on lines +127 to 128
expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10)
expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10)
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 | ⚡ Quick win

Duplicate assertion — likely copy-paste error.

Line 128 repeats the same assertion as line 127. Given the test expects 2 calls to coreRowModelFn, the second assertion should probably verify the second call's row count.

🐛 Proposed fix
         expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10)
-        expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10)
+        expect(coreRowModelFn.mock.calls[1]![0].rows.length).toEqual(10)
📝 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
expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10)
expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10)
expect(coreRowModelFn.mock.calls[0]![0].rows.length).toEqual(10)
expect(coreRowModelFn.mock.calls[1]![0].rows.length).toEqual(10)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-table/tests/injectTable.test.ts` around lines 127 - 128, The
second duplicate assertion should check the second invocation of the mock
instead of repeating the first; update the duplicate expect to reference
coreRowModelFn.mock.calls[1] (the second call) and assert its [0].rows.length
equals 10 so the test verifies both calls to coreRowModelFn.

Comment on lines +16 to +46
function signalToReadonlyAtom<T>(source: {
value: T
subscribe: (observer: (value: T) => void) => () => void
}): ReadonlyAtom<T> {
return Object.assign(source, {
get: () => source.value,
subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
const unsubscribe = source.subscribe(observerToCallback(observerOrNext))
return { unsubscribe }
}) as ReadonlyAtom<T>['subscribe'],
})
}

function signalToWritableAtom<T>(source: {
value: T
subscribe: (observer: (value: T) => void) => () => void
}): Atom<T> {
return Object.assign(source, {
set: (updater: T | ((prevVal: T) => T)) => {
source.value =
typeof updater === 'function'
? (updater as (prevVal: T) => T)(source.value)
: updater
},
get: () => source.value,
subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
const unsubscribe = source.subscribe(observerToCallback(observerOrNext))
return { unsubscribe }
}) as Atom<T>['subscribe'],
})
}
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 | 🔴 Critical | ⚡ Quick win

Critical: subscribe wrapper infinitely recurses — stack overflow on first subscription.

Object.assign(source, { subscribe: wrapper }) replaces source.subscribe (the native Preact Signal.prototype.subscribe, accessed as a closure-captured property lookup) with wrapper before any caller can invoke it. When wrapper runs, the expression source.subscribe(...) resolves to wrapper itself (own-property shadows the prototype), not the original Preact method → infinite recursion → stack overflow.

The test only exercises .get() / .set(), so this is not caught today. Any framework subscriber (or internal table wiring) that calls .subscribe() on one of these atoms will crash immediately.

The fix is to capture the original native subscribe before the assignment in both signalToReadonlyAtom and signalToWritableAtom:

🐛 Proposed fix
 function signalToReadonlyAtom<T>(source: {
   value: T
   subscribe: (observer: (value: T) => void) => () => void
 }): ReadonlyAtom<T> {
+  const _nativeSubscribe = source.subscribe.bind(source)
   return Object.assign(source, {
     get: () => source.value,
     subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
-      const unsubscribe = source.subscribe(observerToCallback(observerOrNext))
+      const unsubscribe = _nativeSubscribe(observerToCallback(observerOrNext))
       return { unsubscribe }
     }) as ReadonlyAtom<T>['subscribe'],
   })
 }

 function signalToWritableAtom<T>(source: {
   value: T
   subscribe: (observer: (value: T) => void) => () => void
 }): Atom<T> {
+  const _nativeSubscribe = source.subscribe.bind(source)
   return Object.assign(source, {
     set: (updater: T | ((prevVal: T) => T)) => {
       source.value =
         typeof updater === 'function'
           ? (updater as (prevVal: T) => T)(source.value)
           : updater
     },
     get: () => source.value,
     subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
-      const unsubscribe = source.subscribe(observerToCallback(observerOrNext))
+      const unsubscribe = _nativeSubscribe(observerToCallback(observerOrNext))
       return { unsubscribe }
     }) as Atom<T>['subscribe'],
   })
 }
📝 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
function signalToReadonlyAtom<T>(source: {
value: T
subscribe: (observer: (value: T) => void) => () => void
}): ReadonlyAtom<T> {
return Object.assign(source, {
get: () => source.value,
subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
const unsubscribe = source.subscribe(observerToCallback(observerOrNext))
return { unsubscribe }
}) as ReadonlyAtom<T>['subscribe'],
})
}
function signalToWritableAtom<T>(source: {
value: T
subscribe: (observer: (value: T) => void) => () => void
}): Atom<T> {
return Object.assign(source, {
set: (updater: T | ((prevVal: T) => T)) => {
source.value =
typeof updater === 'function'
? (updater as (prevVal: T) => T)(source.value)
: updater
},
get: () => source.value,
subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
const unsubscribe = source.subscribe(observerToCallback(observerOrNext))
return { unsubscribe }
}) as Atom<T>['subscribe'],
})
}
function signalToReadonlyAtom<T>(source: {
value: T
subscribe: (observer: (value: T) => void) => () => void
}): ReadonlyAtom<T> {
const _nativeSubscribe = source.subscribe.bind(source)
return Object.assign(source, {
get: () => source.value,
subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
const unsubscribe = _nativeSubscribe(observerToCallback(observerOrNext))
return { unsubscribe }
}) as ReadonlyAtom<T>['subscribe'],
})
}
function signalToWritableAtom<T>(source: {
value: T
subscribe: (observer: (value: T) => void) => () => void
}): Atom<T> {
const _nativeSubscribe = source.subscribe.bind(source)
return Object.assign(source, {
set: (updater: T | ((prevVal: T) => T)) => {
source.value =
typeof updater === 'function'
? (updater as (prevVal: T) => T)(source.value)
: updater
},
get: () => source.value,
subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
const unsubscribe = _nativeSubscribe(observerToCallback(observerOrNext))
return { unsubscribe }
}) as Atom<T>['subscribe'],
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/preact-table/src/signals.ts` around lines 16 - 46, The subscribe
wrapper currently shadows and replaces the original Preact subscribe method
causing infinite recursion; in both signalToReadonlyAtom and
signalToWritableAtom capture the original subscribe into a local (e.g. const
nativeSubscribe = source.subscribe) before calling Object.assign, then implement
the wrapper to call nativeSubscribe(observerToCallback(...)) instead of
source.subscribe(...); keep the rest of the wrapper behavior (returning {
unsubscribe }) and ensure get/set remain unchanged.

Comment on lines +142 to +147
useEffect(() => {
table.setOptions((prev) => ({
...prev,
...tableOptions,
}))
}, [table, tableOptions])
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

🧩 Analysis chain

🌐 Web query:

In React, are updates made inside useEffect visible during the same render, or only after the commit phase? What does that imply for derived values that are read during render?

💡 Result:

In React, updates made inside useEffect are not visible during the same render. They only take effect after the commit phase, triggering a new render. useEffect runs after the render and commit phases complete—specifically, after the browser has painted the screen (for useEffect; useLayoutEffect runs before paint but still after render). Any state updates via setState inside useEffect schedule a re-render for the next cycle. The current render uses the state values from before the effect ran. This implies that derived values read during render cannot depend on state updated in useEffect from the same render cycle. They will see stale values. To ensure derived values reflect effect-updated state, compute them in a subsequent render (e.g., via another useEffect or directly in the component body using current state/props). For values purely derived from props/state without side effects, compute them during render instead of using effects. Example: function MyComponent({ prop }) { const [state, setState] = useState(initial); const derived = computeFrom(prop, state); // Uses current render's state/prop useEffect( => { const newState = computeNewState(prop, state); setState(newState); // Queues re-render; derived above doesn't see this yet }, [prop]); return

{derived}
; // First render: old derived; second: new derived } On re-render triggered by the effect, derived will now use the updated state. Prefer deriving during render over effects for performance and correctness (avoids extra renders/loops).

Citations:


🏁 Script executed:

# First, let's understand the structure
cd packages/react-table && find . -name "*.ts" -o -name "*.tsx" | head -20

Repository: TanStack/table

Length of output: 280


🏁 Script executed:

# Search for getRowModel and getHeaderGroups usage
rg "getRowModel|getHeaderGroups" --type ts --type tsx -B 2 -A 2 | head -60

Repository: TanStack/table

Length of output: 86


🏁 Script executed:

# Check the useTable hook implementation to see the full context
cat -n packages/react-table/src/useTable.ts | head -160

Repository: TanStack/table

Length of output: 6693


🏁 Script executed:

# Look for where table instance is created and returned
rg "createTable|new Table" packages/react-table/src/useTable.ts -B 3 -A 3

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Search for getRowModel and getHeaderGroups to understand when they're called
rg "getRowModel|getHeaderGroups" --type ts -B 2 -A 2 | head -100

Repository: TanStack/table

Length of output: 8845


🏁 Script executed:

# Check the table-core implementation to see if these are computed on-demand
fd -e ts -e tsx | grep -i "table-core" | head -5

Repository: TanStack/table

Length of output: 312


🏁 Script executed:

# Search for setOptions implementation and how it affects computed values
rg "setOptions" packages/react-table/src/ -B 2 -A 5

Repository: TanStack/table

Length of output: 484


🏁 Script executed:

# Check git history for setOptions changes in useTable
git log --oneline -20 -- packages/react-table/src/useTable.ts

Repository: TanStack/table

Length of output: 88


🏁 Script executed:

# Check if useLayoutEffect is imported or used elsewhere in react-table
rg "useLayoutEffect" packages/react-table/src/

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Look at the full context of the Svelte implementation to understand the pattern
rg -B 5 -A 10 "effect.pre" packages/svelte-table/src/

Repository: TanStack/table

Length of output: 1929


🏁 Script executed:

# Check if there are any tests that verify the option synchronization timing
rg "setOptions" packages/table-core -B 3 -A 5 --type ts | head -80

Repository: TanStack/table

Length of output: 4442


Use pre-commit timing for option synchronization, not post-commit.

Moving table.setOptions(...) into useEffect delays option updates until after commit, causing one-render staleness. If consumers call getRowModel(), getHeaderGroups(), or other computed methods during the same render cycle as a prop change, they will observe stale options. The Svelte adapter in this repository already acknowledges this issue and uses pre-commit timing ($effect.pre) with explicit documentation that post-commit timing causes "one-frame-behind data." Consider useLayoutEffect instead, which runs after render but before paint, ensuring option updates are visible to downstream render passes while avoiding render-phase mutations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-table/src/useTable.ts` around lines 142 - 147, The current
useEffect block that calls table.setOptions(({...prev, ...tableOptions})) causes
post-commit timing and one-render staleness for computed methods like
getRowModel() and getHeaderGroups(); change this to useLayoutEffect so options
are synchronized pre-paint (before the browser paints) and visible within the
same render pass—replace the useEffect that references table.setOptions, table,
and tableOptions with useLayoutEffect and keep the same merge logic to avoid
render-phase mutations.

Comment on lines 77 to 79
const mergedOptions = mergeProps(tableOptions, {
_features: mergeProps(tableOptions._features, {
solidReactivityFeature,
}),
reactivity: solidReactivity(owner),
}) as any
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

🧩 Analysis chain

🌐 Web query:

In SolidJS, when the same key exists in multiple mergeProps(...) sources, which source takes precedence? Please confirm whether later sources override earlier ones.

💡 Result:

In SolidJS, when the same key exists in multiple mergeProps(...) sources, the last (later) source takes precedence. It merges prop sources from left to right, and the last source with a non-undefined value for a property wins. Later sources override earlier ones.

Citations:


Let caller-provided reactivity win by swapping the merge order.

In SolidJS, mergeProps gives later sources precedence. The current order mergeProps(tableOptions, { reactivity: solidReactivity(owner) }) always overwrites any caller-provided tableOptions.reactivity with the default, which breaks the contract this PR establishes for allowing external/native reactivity bindings.

Suggested fix
-  const mergedOptions = mergeProps(tableOptions, {
-    reactivity: solidReactivity(owner),
-  }) as any
+  const mergedOptions = mergeProps(
+    { reactivity: solidReactivity(owner) },
+    tableOptions,
+  ) as any
📝 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
const mergedOptions = mergeProps(tableOptions, {
_features: mergeProps(tableOptions._features, {
solidReactivityFeature,
}),
reactivity: solidReactivity(owner),
}) as any
const mergedOptions = mergeProps(
{ reactivity: solidReactivity(owner) },
tableOptions,
) as any
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-table/src/createTable.ts` around lines 77 - 79, The merge
order currently forces the default reactivity to override caller-provided
values; change the merge so the default comes first and caller options last
(swap the args to mergeProps) so caller-provided reactivity wins: update the
mergedOptions creation that calls mergeProps(tableOptions, { reactivity:
solidReactivity(owner) }) to call mergeProps({ reactivity:
solidReactivity(owner) }, tableOptions) instead, keeping the same symbols
mergedOptions, mergeProps, tableOptions, solidReactivity(owner), and the
reactivity property.

Comment on lines +21 to +32
export type ConstructTable<
TFeatures extends TableFeatures,
TData extends RowData,
> = Omit<TableOptions<TFeatures, TData>, 'reactivity'> & {
reactivity: TableReactivityBindings
}

export function constructTable<
TFeatures extends TableFeatures,
TData extends RowData,
>(tableOptions: TableOptions<TFeatures, TData>): Table<TFeatures, TData> {
>(tableOptions: ConstructTable<TFeatures, TData>): Table<TFeatures, TData> {
const signals = tableOptions.reactivity
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

Keep reactivity backward-compatible on the public constructTable API.

Making reactivity mandatory turns this into a source-compatible break for every direct @tanstack/table-core consumer, even when they just want the old default behavior. Since the PR goal is to add external bindings support, this should either keep a default binding here or be called out explicitly as a breaking migration surface before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/src/core/table/constructTable.ts` around lines 21 - 32,
The public API currently requires reactivity by changing the ConstructTable type
and constructTable parameter to include a mandatory reactivity, which is a
breaking change; revert this by making reactivity optional on the public
ConstructTable/constructTable signature (e.g., keep
TableOptions<TFeatures,TData> & { reactivity?: TableReactivityBindings }) and
inside constructTable (the function using tableOptions.reactivity) default to
the existing internal bindings when reactivity is undefined (create or assign
the default TableReactivityBindings before using signals). Ensure you reference
and update ConstructTable, constructTable, TableOptions, and
TableReactivityBindings so consumers without explicit reactivity continue to get
the previous behavior.

Comment on lines +112 to +115
/**
* Table custom reactibity bindings.
*/
readonly reactivity?: TableReactivityBindings
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 | ⚡ Quick win

Typo in JSDoc: "reactibity" → "reactivity".

-   * Table custom reactibity bindings.
+   * Table custom reactivity bindings.
📝 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
/**
* Table custom reactibity bindings.
*/
readonly reactivity?: TableReactivityBindings
/**
* Table custom reactivity bindings.
*/
readonly reactivity?: TableReactivityBindings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/src/core/table/coreTablesFeature.types.ts` around lines
112 - 115, Fix the JSDoc typo for the Table custom reactibity bindings by
changing "reactibity" to "reactivity" above the readonly reactivity?:
TableReactivityBindings property; update the comment text so it reads "Table
custom reactivity bindings." to match the property name and type
(TableReactivityBindings) and ensure consistency across any nearby docs/comments
referencing this symbol.

Comment on lines +61 to +74
export function atomToStore<T>(
atom: Atom<T> | ReadonlyAtom<T>,
): Store<T> | ReadonlyStore<T> {
const store: Store<T> = atom as Store<T>
Object.defineProperty(atom, 'state', {
get() {
return atom.get()
},
})
if ('set' in atom) {
store.setState = atom.set.bind(atom)
}
return store
}
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

atomToStore mutates its input and will throw on a second call with the same atom.

Two concrete problems:

  1. Object.defineProperty defaults to configurable: false, so if atomToStore is called with the same Atom instance a second time (e.g., because constructTable is somehow invoked again with a memoized atom, or in a test harness), the runtime throws TypeError: Cannot redefine property: state.

  2. const store: Store<T> = atom as Store<T>store and atom are the same object. Every mutation (Object.defineProperty, store.setState = ...) permanently alters the original Atom, making the function an impure side-effecting operation rather than a pure bridge/adapter.

Replace the in-place mutation with a thin wrapper that inherits the atom's prototype chain:

🛡️ Proposed fix
 export function atomToStore<T>(
   atom: Atom<T> | ReadonlyAtom<T>,
 ): Store<T> | ReadonlyStore<T> {
-  const store: Store<T> = atom as Store<T>
-  Object.defineProperty(atom, 'state', {
+  const store = Object.create(atom) as Store<T>
+  Object.defineProperty(store, 'state', {
     get() {
       return atom.get()
     },
+    configurable: true,
+    enumerable: true,
   })
   if ('set' in atom) {
-    store.setState = atom.set.bind(atom)
+    store.setState = (atom as Atom<T>).set.bind(atom as Atom<T>)
   }
   return store
 }

Using Object.create(atom) keeps atom unmodified, all atom methods remain accessible through the prototype chain, and the state/setState additions live only on the wrapper object. Adding configurable: true also makes repeated wrapping safe.

📝 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 function atomToStore<T>(
atom: Atom<T> | ReadonlyAtom<T>,
): Store<T> | ReadonlyStore<T> {
const store: Store<T> = atom as Store<T>
Object.defineProperty(atom, 'state', {
get() {
return atom.get()
},
})
if ('set' in atom) {
store.setState = atom.set.bind(atom)
}
return store
}
export function atomToStore<T>(
atom: Atom<T> | ReadonlyAtom<T>,
): Store<T> | ReadonlyStore<T> {
const store = Object.create(atom) as Store<T>
Object.defineProperty(store, 'state', {
get() {
return atom.get()
},
configurable: true,
enumerable: true,
})
if ('set' in atom) {
store.setState = (atom as Atom<T>).set.bind(atom as Atom<T>)
}
return store
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/src/features/table-reactivity/table-reactivity.ts` around
lines 61 - 74, The function atomToStore mutates the incoming Atom (using
Object.defineProperty on atom and assigning store.setState), which can throw on
subsequent calls and corrupt the original Atom; change it to return a thin
wrapper object that inherits from the atom (e.g., wrapper = Object.create(atom))
so the original atom is not mutated, define the 'state' accessor on the wrapper
with configurable: true, and assign wrapper.setState (not atom or store
referencing the same object) to atom.set.bind(atom) when a setter exists; keep
all other atom methods available via the prototype chain so atomToStore remains
a pure adapter.

Comment on lines +60 to +61
untrack: (fn) => fn(),
batch: (fn) => fn(),
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all call-sites of `untrack` inside table-core to understand
# whether it is called in reactive contexts (computed/watchEffect) that would
# cause wrong behavior with a no-op implementation.

rg -n --type=ts "untrack" packages/table-core/src/ -A 3 -B 3

Repository: TanStack/table

Length of output: 1500


🏁 Script executed:

# First, let's see the actual implementation in packages/vue-table/src/signals.ts
cat -n packages/vue-table/src/signals.ts | head -70

Repository: TanStack/table

Length of output: 2411


🏁 Script executed:

# Search for actual usages/calls of untrack in the codebase
rg "\.untrack\(" --type=ts -B 3 -A 3

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check if there are other implementations of untrack in different bindings
rg -n "untrack.*=|untrack:" --type=ts -B 2 -A 2

Repository: TanStack/table

Length of output: 5527


🏁 Script executed:

# Look for any imports of pauseTracking or resetTracking to see if they're already being used
rg "pauseTracking|resetTracking" --type=ts

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check package.json for `@vue/reactivity`
cat packages/vue-table/package.json | grep -A 5 -B 5 "vue\|reactivity"

Repository: TanStack/table

Length of output: 1032


🏁 Script executed:

# Look for any existing imports of `@vue/reactivity` in the codebase
rg "@vue/reactivity" --type=ts

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check the root package.json to see Vue version
cat package.json | jq '.devDependencies."vue" // .dependencies."vue" // empty'

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Search for how other Vue files handle reactive context management
rg "computed|watch|watchEffect" packages/vue-table/src/ --type=ts -l

Repository: TanStack/table

Length of output: 127


🏁 Script executed:

# Check the Vue type definitions to see if untrack is exported as public API
cat packages/vue-table/node_modules/vue/dist/vue.d.ts 2>/dev/null | grep -i "untrack" || echo "Vue type definitions not available in sandbox"

Repository: TanStack/table

Length of output: 104


🏁 Script executed:

# Check Vue 3.5+ release notes or source - search for untrack in any documentation files
find . -name "*.md" -type f -exec grep -l "untrack" {} \; 2>/dev/null | head -5

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Let's check what actually gets exported from 'vue' in the current import statement
rg "export.*untrack|export.*from.*vue" packages/vue-table/src/ --type=ts

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check if there's any Vue documentation or examples using untrack
rg "import.*untrack.*from.*['\"]vue['\"]" --type=ts

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Look at the actual Vue table store to understand the contract better
cat packages/vue-table/src/useTable.ts | head -50

Repository: TanStack/table

Length of output: 1521


🏁 Script executed:

# Let's see how untrack is actually used in table-core to understand the context better
rg -B 5 -A 5 "\.untrack\(" packages/table-core/src/ --type=ts

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check if there's a watch or watchEffect that might call setOptions
rg "watch|watchEffect" packages/table-core/src/ --type=ts | head -20

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Look at Vue 3 official documentation or check if there's any version info about untrack
web_search
Vue 3 untrack API reactive tracking suppression

Repository: TanStack/table

Length of output: 151


🌐 Web query:

Vue 3 untrack pauseTracking resetTracking internal API

💡 Result:

Vue 3's reactivity system includes internal APIs for controlling dependency tracking: pauseTracking, resetTracking, and untrack. These are not part of the public API documented in official Vue docs but exist in the @vue/reactivity source code and are used internally or by advanced users. pauseTracking and resetTracking temporarily disable and restore dependency tracking during effect execution. They manipulate a global tracking stack to prevent track calls from collecting dependencies. - pauseTracking: Pushes current shouldTrack state (false) onto trackStack. - resetTracking: Pops the stack to restore previous tracking state. Usage example from community code: function untrack(fn) { pauseTracking try { return fn } finally { resetTracking } } untrack is a newer utility (PR #13286, open as of 2025) that executes a callback without tracking dependencies, likely built on similar internals. These are internal and exporting them publicly was proposed (PR #13072) but closed without merge. They appear in source files like packages/reactivity/src/effect.ts and are used in watch.ts for cleanups. Official docs recommend customRef for controlling tracking in refs or explicit watch sources instead. Use with caution as they affect global state and can lead to inconsistent reactivity graphs if misused. As of 2026-05-02, no public stable API; check latest @vue/reactivity types/source for availability.

Citations:


🏁 Script executed:

# Check the exact usage context in table-core's constructTable to see when untrack is called
rg -n "reactivity\\.untrack" packages/table-core/src/ -B 10 -A 5

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Look for setOptions usage pattern to understand the reactive context
rg -n "setOptions" packages/table-core/src/core/table.ts -B 5 -A 8 | head -50

Repository: TanStack/table

Length of output: 136


🏁 Script executed:

# Find where setOptions is defined
fd "table.ts" packages/table-core/src/ --type f

Repository: TanStack/table

Length of output: 150


🏁 Script executed:

# Look for the actual file structure in table-core
ls -la packages/table-core/src/core/ 2>/dev/null || find packages/table-core/src/ -name "*.ts" -type f | head -20

Repository: TanStack/table

Length of output: 563


🏁 Script executed:

# Search for how reactivity.untrack is called in table-core
rg "reactivity" packages/table-core/src/ --type=ts -A 3 -B 3 | grep -A 5 -B 5 "untrack"

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Look at constructTable.ts to find untrack usage
cat packages/table-core/src/core/table/constructTable.ts | head -100

Repository: TanStack/table

Length of output: 3504


🏁 Script executed:

# Search more broadly for where reactivity object methods are called
rg "options\\.untrack|reactivity\.untrack|state\.untrack" --type=ts -B 3 -A 3

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check if there are any watch or computed calls in the core that might need untrack
rg "watch|computed|effect" packages/table-core/src/core/table/constructTable.ts

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Continue reading constructTable.ts to find untrack usage
cat packages/table-core/src/core/table/constructTable.ts | tail -100

Repository: TanStack/table

Length of output: 2962


🏁 Script executed:

# Search for setOptions implementation which likely uses untrack
rg -n "setOptions" packages/table-core/src/core/table/constructTable.ts -B 5 -A 10

Repository: TanStack/table

Length of output: 697


🏁 Script executed:

# Look for where untrack is actually invoked in any of the features
rg "untrack\(" packages/table-core/src/ --type=ts -B 5 -A 5

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Search in features directory for untrack usage
rg "untrack" packages/table-core/src/features/ --type=ts -B 3 -A 3

Repository: TanStack/table

Length of output: 1464


🏁 Script executed:

# Check if untrack is used anywhere in table-core at all
rg "untrack" packages/table-core/ --type=ts

Repository: TanStack/table

Length of output: 258


🏁 Script executed:

# Look at what features actually use reactivity
rg "reactivity\." packages/table-core/src/features/ --type=ts | head -20

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Double-check - search across ALL frameworks' binding files for actual untrack calls
rg "\.untrack\(" --type=ts

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check the interface definition and see if there are any comments about usage
cat packages/table-core/src/features/table-reactivity/table-reactivity.ts

Repository: TanStack/table

Length of output: 1935


untrack is a no-op that does not actually suppress Vue reactive tracking.

Vue 3 has no public untrack API. Calling fn() directly does not pause Vue's dependency-tracking mechanism. While this is not currently an issue since untrack() is not invoked anywhere in the codebase, the implementation violates the interface contract: "Evaluates a function without tracking reactive dependencies." Other frameworks (Solid, Svelte, Angular, Preact) all provide proper implementations.

Choose a remedy:

Option A: Use `@vue/reactivity` internals (undocumented but functional)
import { pauseTracking, resetTracking } from '@vue/reactivity'

untrack: (fn) => {
  pauseTracking()
  try {
    return fn()
  } finally {
    resetTracking()
  }
}
Option B: Document the limitation

Add a comment explaining that Vue's untrack cannot suppress dependencies and table-core should avoid relying on it inside reactive contexts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-table/src/signals.ts` around lines 60 - 61, The current untrack
implementation (untrack: (fn) => fn()) does not suppress Vue reactive tracking;
replace it with a proper pause/reset pair from Vue internals by importing
pauseTracking and resetTracking from '@vue/reactivity' and implement untrack to
call pauseTracking(), invoke fn() in a try block, and call resetTracking() in
finally so tracking is always restored; keep batch as-is or document its
behavior if you choose not to change it.

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.

2 participants