Skip to content

feat(table): support external reactivity binding#6237

Merged
KevinVandy merged 17 commits intoalphafrom
feat/native-reactivity
May 4, 2026
Merged

feat(table): support external reactivity binding#6237
KevinVandy merged 17 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

  • New Features

    • Unified reactivity bindings and framework adapters added; public reactivity entry exported.
  • Bug Fixes & Improvements

    • Improved table reactivity, lifecycle, and option synchronization across frameworks.
    • Example filter inputs now use onInput for more immediate updates.
  • Chores

    • Dependency bumps (zod, dayjs, eslint plugin); added Preact signals; removed benchmark artifacts and color‑scheme meta tags.
  • Tests

    • Added and re-enabled unit/integration tests for reactivity integrations.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f85462f7-0703-491b-815f-f11e4075caa2

📥 Commits

Reviewing files that changed from the base of the PR and between 781e5bc and b804881.

📒 Files selected for processing (1)
  • packages/react-table/src/useLegacyTable.ts

📝 Walkthrough

Walkthrough

Replaces notifier-based reactivity with a TableReactivityBindings abstraction, adds framework-specific reactivity adapters (Angular, Preact, React, Solid, Svelte, Vue), refactors core table internals to consume these bindings, updates many tests/helpers to inject the new bindings, and applies assorted example/UI and dependency tweaks.

Changes

Core reactivity migration

Layer / File(s) Summary
Reactivity Types
packages/table-core/src/core/reactivity/coreReactivityFeature.types.ts
Adds TableReactivityBindings and TableAtomOptions types for adapter primitives (createWritableAtom, createReadonlyAtom, untrack, batch).
Core Bindings Implementation
packages/table-core/src/core/reactivity/constructReactivityBindings.ts
Adds constructReactivityBindings() implementing bindings using TanStack store atoms.
Atom ↔ Store Utilities
packages/table-core/src/core/reactivity/coreReactivityFeature.utils.ts
Adds atomToStore helpers converting Atom/ReadonlyAtom to Store/ReadonlyStore shapes.
Core Re-exports & Build
packages/table-core/src/reactivity.ts, packages/table-core/package.json, packages/table-core/tsdown.config.ts, packages/table-core/src/index.ts
Re-exports reactivity modules, exposes ./reactivity in package exports, and adds src/reactivity.ts to build entries.
Construct Table Refactor
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
constructTable now consumes coreReativityFeature, creates optionsStore/baseAtoms via adapter atoms, resolves derived atoms from optionsStore.get(), builds table.store with atomToStore, and uses _reactivity.batch/optionsStore.set.
Remove old notifier feature
packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts
Deletes previous notifier/proxy-based constructReactivityFeature implementation.
Tests & helpers updated
packages/table-core/tests/**/*, packages/table-core/tests/helpers/*
Many tests/helpers now inject coreReativityFeature: constructReactivityBindings() and adjust small signatures (e.g., getSubRows), assertions, and expectations.

Framework adapters & adapter surface

Layer / File(s) Summary
Adapter types / core feature hook
packages/table-core/src/core/coreFeatures.ts
Adds optional coreReativityFeature?: TableReactivityBindings to CoreFeatures.
Angular adapter & inject table
packages/angular-table/src/reactivity.ts, packages/angular-table/src/injectTable.ts, packages/angular-table/src/helpers/createTableHook.ts
Adds angularReactivity(injector) adapter; injects it into _features; rewrites injectTable to construct table with adapter and sync options in a single effect; table.computed/table.state/table.value shapes updated.
React / Preact adapters & hooks
packages/react-table/src/useTable.ts, packages/react-table/src/useLegacyTable.ts, packages/preact-table/src/useTable.ts, packages/preact-table/src/reactivity.ts, packages/preact-table/package.json
useTable implementations now inject constructReactivityBindings() (or adapter in Preact), move options sync into effects, and derive options from optionsStore; useLegacyTable caches _rowModels in useState. Adds preactReactivity() and @preact/signals dep.
Solid adapter & createTable
packages/solid-table/src/reactivity.ts, packages/solid-table/src/createTable.ts
Adds solidReactivity(owner) and injects it; removes previous notifier/signal workaround and uses untrack + table.setOptions in computed synchronization.
Svelte adapter & createTable
packages/svelte-table/src/reactivity.svelte.ts, packages/svelte-table/src/createTable.svelte.ts
Adds svelteReactivity() and injects it; replaces notifier subscriptions with a single $effect.pre that reads reactive inputs and applies table.setOptions inside untrack.
Vue adapter & useTable
packages/vue-table/src/reactivity.ts, packages/vue-table/src/useTable.ts, packages/vue-table/tests/unit/signals.test.ts
Adds vueReactivity() and injects it; replaces watchEffect/notifier approach with targeted watch calls for option dependency and controlled-state sync; adds unit test for bindings.
Lit adapter
packages/lit-table/src/TableController.ts, packages/lit-table/tests/unit/defaultReactivity.test.ts
Switches controller to constructReactivityBindings() and adds unit test asserting default reactivity wiring.

Angular tests, config, and benchmarks

Layer / File(s) Summary
Angular tests
packages/angular-table/tests/angularReactivityFeature.test.ts, packages/angular-table/tests/injectTable.test.ts, packages/angular-table/tests/flex-render/*
Re-enabled/updated Angular reactivity integration test expectations; adapt helper option composition and expected call counts.
Vite & scripts
packages/angular-table/vite.config.ts, packages/angular-table/package.json
Drops jit: true from Angular Vite plugin and removes test:benchmark script.
Benchmarks removed
packages/angular-table/tests/benchmarks/*
Deleted benchmark harness and its setup utilities.

Examples, UI handlers, and misc updates

Layer / File(s) Summary
Preact example input handlers
examples/preact/**/src/**/*.tsx
Changed multiple example input handlers from onChange to onInput (DebouncedInput, Filter, ColumnFilter).
Preact example HTML
examples/preact/**/index.html
Removed <meta name="color-scheme" content="light dark" /> from many Preact example HTML files.
Example dependency bumps
examples/react/mantine-react-table/package.json, examples/**/with-tanstack-form/package.json, examples/**/with-tanstack-router/package.json
Bumped dayjs, zod, and @tanstack/router-vite-plugin in various examples.
Misc dependency bumps
packages/react-table/package.json, packages/react-table-devtools/package.json
Bumped @eslint-react/eslint-plugin.
Preact package dependency
packages/preact-table/package.json
Added @preact/signals.

Sequence Diagram(s)

sequenceDiagram
    participant App as App / Framework
    participant Adapter as Framework Adapter (useTable / injectTable)
    participant Core as constructTable / Table Core
    participant Reactivity as TableReactivityBindings
    participant Table as Table Instance

    App->>Adapter: call useTable/injectTable(tableOptions)
    Adapter->>Core: constructTable({...tableOptions, _features.coreReativityFeature})
    Core->>Reactivity: create optionsStore/atoms via bindings
    Adapter->>Table: observe state/options and call table.setOptions(merged) on changes
    Reactivity->>Core: batch / untrack used during internal updates
    Table->>Adapter: expose state/options via store/atoms -> propagate to App
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐇 I hop through atoms, signals, and queues,
I stitch frameworks' beats into one set of cues.
From core to examples the bindings sing—
a tiny rabbit cheers: let reactivity spring! 🥕

✨ 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

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 22, 2026

View your CI Pipeline Execution ↗ for commit ae7eb1f

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

☁️ Nx Cloud last updated this comment at 2026-05-03 03:30:21 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: b804881

@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
nx-cloud[bot]

This comment was marked as outdated.

@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 thread packages/table-core/src/core/table/coreTablesFeature.types.ts Outdated
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.

nx-cloud[bot]

This comment was marked as outdated.

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 updated five areas introduced by this PR to resolve failing type checks, runtime import errors, and the knip unresolved-import failure. The fixes correct a reactivity_reactivity typo, remove erroneous readonly modifiers from lazily-assigned prototype caches, add the missing reactivity option to TableOptions_Table, redirect constructTable to read from the new option, rename all test references from the non-existent defaultReactivityBindings module to the actual constructReactivityBindings, and update the vue-table signals test to import from the renamed reactivity.ts file.

Warning

  • We could not verify this fix.
  • The suggested diff is too large to display here, but you can view it on Nx Cloud ↗

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally XWKB-YFhH

Apply fix locally with your editor ↗   View interactive diff ↗



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

@KevinVandy KevinVandy marked this pull request as ready for review May 3, 2026 02:50
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/angular-table/src/helpers/createTableHook.ts (1)

445-455: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

User-provided coreReativityFeature in defaultTableOptions._features silently overrides Angular's injected reactivity.

injectAppTable now forwards { ...defaultTableOptions._features, appTableFeatures } as options()._features. Inside injectTable.ts (line 136–143), the merge order is:

_features: {
  coreReativityFeature: angularReactivity(injector),  // injected first
  ...options()._features,                              // user features spread after → wins
}

Because CreateTableContextOptions does not exclude _features, a consumer of createTableHook can pass _features: { coreReativityFeature: someCustomBinding } in defaultTableOptions. That will propagate into options()._features and silently replace angularReactivity(injector), defeating the framework adapter entirely.

The fix belongs in packages/angular-table/src/injectTable.ts — Angular's binding should be placed last so it always takes precedence:

 _features: {
-  coreReativityFeature: angularReactivity(injector),
   ...options()._features,
+  coreReativityFeature: angularReactivity(injector),
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-table/src/helpers/createTableHook.ts` around lines 445 -
455, The merge in injectTable (injectTable.ts) currently spreads
options()._features after inserting the framework reactivity, which allows a
user-provided _features.coreReativityFeature to override Angular's
angularReactivity(injector); change the merge so Angular's binding wins by
applying the user features first and then setting coreReativityFeature:
angularReactivity(injector) (i.e., merge ...options()._features before, and
explicitly assign coreReativityFeature = angularReactivity(injector) afterwards)
so angularReactivity always takes precedence; reference injectTable,
coreReativityFeature, angularReactivity(injector) and options()._features when
making the change.
packages/svelte-table/src/createTable.svelte.ts (1)

67-83: ⚠️ Potential issue | 🟠 Major

The $effect.pre only tracks state and data dependencies, leaving other reactive options like columns unsynced.

If columns or other option properties are passed as reactive getters (e.g., $state or derived), changes to them won't retrigger the effect, so table.setOptions(...) remains stale. Since the effect merges all of mergedOptions via setOptions, dependency reads should cover all reactive properties, not just state and data.

🤖 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 67 - 83, The
effect only reads mergedOptions.state and mergedOptions.data so reactive getters
like mergedOptions.columns won't become dependencies; fix by, inside the
existing $effect.pre callback that currently reads state/data (before calling
table.setOptions/untrack), iterate over all keys of mergedOptions and access
each value (e.g., for (const k in mergedOptions) void mergedOptions[k]) so any
reactive getters on mergedOptions (columns, other option props) are tracked and
will retrigger the effect that calls table.setOptions; keep the existing
untrack/setOptions placement unchanged.
♻️ Duplicate comments (2)
packages/preact-table/src/reactivity.ts (1)

17-47: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: subscribe wrapper causes infinite recursion — stack overflow on first subscription.

Object.assign(source, { subscribe: wrapper }) replaces source.subscribe with wrapper before it is invoked. When wrapper executes, source.subscribe(...) on lines 24 and 43 now resolves to wrapper itself (own-property shadows the prototype), causing infinite recursion.

This was flagged in a previous review and remains unfixed.

🐛 Proposed fix — capture native subscribe before assignment
 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'],
   })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/preact-table/src/reactivity.ts` around lines 17 - 47, The subscribe
wrapper in signalToReadonlyAtom and signalToWritableAtom overwrites
source.subscribe before calling it, causing infinite recursion; fix by capturing
the original subscribe function into a local variable (e.g., const
nativeSubscribe = source.subscribe) before calling Object.assign, then use
nativeSubscribe(observerToCallback(...)) inside the new subscribe wrapper; keep
the rest of the wrapper behavior (observerToCallback conversion and returning {
unsubscribe }) unchanged so semantics match the original intent.
packages/vue-table/src/reactivity.ts (1)

60-61: ⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

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

Vue 3 has no public untrack API. The current implementation (fn) => fn() does not pause dependency tracking, violating the TableReactivityBindings contract. While this is not immediately broken since untrack isn't called in reactive contexts currently, it's a latent correctness issue.

This was flagged in a previous review. Consider using @vue/reactivity internals or documenting the limitation.

Option A: Use `@vue/reactivity` internals
import { pauseTracking, resetTracking } from '@vue/reactivity'

untrack: (fn) => {
  pauseTracking()
  try {
    return fn()
  } finally {
    resetTracking()
  }
}
Option B: Document the limitation
-    untrack: (fn) => fn(),
+    // Vue 3 has no public untrack API. This is a no-op; table-core should
+    // avoid calling untrack() inside Vue reactive contexts (computed/watch).
+    untrack: (fn) => fn(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-table/src/reactivity.ts` around lines 60 - 61, The current
untrack implementation in TableReactivityBindings (untrack: (fn) => fn()) is a
no-op and does not suppress Vue reactive tracking; replace it with a proper
tracking-suppression wrapper by importing pauseTracking and resetTracking from
`@vue/reactivity` and invoking pauseTracking before calling the passed fn and
resetTracking in a finally block, or if you prefer not to use internal APIs,
explicitly document the limitation in the TableReactivityBindings comment and
keep untrack as a documented noop; ensure the change references the untrack
symbol so behavior matches the contract and leave batch unchanged.
🧹 Nitpick comments (2)
packages/table-core/src/core/reactivity/coreReactivityFeature.utils.ts (1)

16-29: atomToStore permanently mutates the input atom — use a wrapper object instead.

store is just a re-cast alias of atom (same reference). The Object.defineProperty call and the store.setState = ... assignment both modify the original atom directly, causing callers' atoms to acquire unwanted state and setState side effects. A wrapper avoids polluting the original object.

Additionally, Atom<T>.set has a broader signature than Store<T>.setState: it accepts ((prevVal: T) => T) | T, while setState only accepts (prev: T) => T. The as Store<T> cast suppresses this TypeScript incompatibility. A wrapper sidesteps both issues.

♻️ Proposed refactor (wrapper approach)
 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
+  const base: ReadonlyStore<T> = {
+    get state() {
+      return atom.get()
+    },
+  }
+  if ('set' in atom) {
+    ;(base as Store<T>).setState = (atom as Atom<T>).set.bind(atom)
+  }
+  return base as 'set' extends keyof typeof atom ? Store<T> : ReadonlyStore<T>
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/src/core/reactivity/coreReactivityFeature.utils.ts`
around lines 16 - 29, The atomToStore function currently mutates the input atom
by casting it to Store<T> and adding a state getter and setState, which pollutes
the original Atom and hides a signature mismatch between Atom.set and
Store.setState; instead create and return a new wrapper object that implements
Store<T>/ReadonlyStore<T> without modifying the original atom: implement a
wrapper with a state getter that calls atom.get(), a setState method that adapts
Atom.set's broader signature to Store.setState by detecting functions vs values
and delegating to atom.set appropriately, and forward other needed
methods/properties to the original atom (referencing atomToStore, Atom.set and
Store.setState to locate the code to change).
packages/lit-table/src/TableController.ts (1)

98-104: 💤 Low value

Outdated documentation comment references old function name.

The JSDoc mentions constructReactivityFeature but the implementation now uses constructReactivityBindings.

📝 Proposed fix
 /**
  * A Lit ReactiveController for TanStack Table integration.
  *
- * Uses `constructReactivityFeature` from table-core to properly integrate
+ * Uses `constructReactivityBindings` from table-core to properly integrate
  * with the TanStack Store reactivity system, matching the pattern used by
  * all other framework adapters (React, Vue, Solid, Svelte, Angular).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-table/src/TableController.ts` around lines 98 - 104, The JSDoc
in TableController.ts is out of date: it references constructReactivityFeature
while the code now uses constructReactivityBindings; update the documentation
comment to mention constructReactivityBindings (and any relevant behavior name)
so the doc matches the implementation in the TableController class and
surrounding reactivity integration code.
🤖 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/src/injectTable.ts`:
- Around line 59-65: The exported AngularTableComputed type is out of sync with
the new table.computed implementation (which now only accepts a selector +
optional equal), so update or remove AngularTableComputed to match the narrowed
API: remove the old "source" overloads and ensure the type signature exactly
matches computed: <TSubSelected = {}>(props: { selector: (state:
TableState<TFeatures>) => TSubSelected; equal?: ValueEqualityFn<TSubSelected> })
=> Signal<Readonly<TSubSelected>> (also apply the same narrowing for the
duplicate/related type declared near lines 163-169), referencing the
AngularTableComputed identifier and any duplicate interface to keep public types
consistent with table.computed.
- Around line 137-143: Cache the result of options() into a local variable, then
pass that cached options to constructTable while merging _features so that the
Angular reactivity binding overrides any caller-provided feature: take the
cachedOptions._features (if any) and spread them first, then set
coreReativityFeature: angularReactivity(injector) last so it wins; update the
constructTable call (and the cast to AngularTable<TFeatures, TData, TSelected>)
to use cachedOptions and the reordered _features merge to ensure
table.state/table.computed/table.value remain backed by Angular signals.

In `@packages/preact-table/src/reactivity.ts`:
- Line 1: Fix the typo in the top-of-file comment: change the comment text "TOTO
- re-explore preact signals for reactivity" to "TODO - re-explore preact signals
for reactivity" so the intent is clear and searchable; locate the string "TOTO -
re-explore preact signals for reactivity" in the file and update it to "TODO..."
accordingly.

In `@packages/preact-table/src/useTable.ts`:
- Around line 124-135: The hook useTable now returns the raw selector result in
state which can omit required fields (columns, data) from the declared
PreactTable['state'], causing runtime undefined for table.state.columns; fix by
ensuring the returned state always contains the required shape: when building
the return object in useTable (the selector/result used by useSelector and the
returned state), normalize or merge missing fields so state.columns and
state.data are present (e.g., fall back to table.state.columns/table.state.data
or default empty arrays/objects) or update the public type to reflect the new
optional fields — locate useTable, the selector passed into useSelector, and the
returned object spreading table to implement the normalization or update the
type accordingly.

In `@packages/react-table/src/useTable.ts`:
- Around line 152-165: The returned object from useTable (in useTable.ts where
useSelector and useMemo are used to return {...table, options, state}) can
expose a runtime-missing `columns` and `data` despite the public
ReactTable.state type; fix this by normalizing/ensuring the returned state
includes the expected keys (e.g., guarantee state.columns and state.data exist
by merging defaults or falling back to table.initialState or empty arrays)
before returning from useMemo, or alternatively update the public API types and
migration notes if you intend to remove those fields (adjust the return in
useMemo and the selector usage accordingly).

In `@packages/table-core/src/core/coreFeatures.ts`:
- Line 10: The interface marks coreReativityFeature as optional but
constructTable accesses it with non-null assertion, causing silent runtime
failures; fix by either (A) making coreReativityFeature required in the
interface and adding a default coreReativityFeature implementation to the
exported coreFeatures object so callers can safely spread `{ ...coreFeatures }`,
or (B) keep it optional and add a runtime guard in constructTable: read
tableOptions._features.coreReativityFeature into _reactivity and if falsy throw
a clear Error like '[table-core] coreReativityFeature must be provided in
_features' before any use; update references to the symbols
coreReativityFeature, coreFeatures, constructTable, _features, and _reactivity
accordingly.
- Line 10: The public API field is misspelled: rename the symbol
coreReativityFeature to coreReactivityFeature across the codebase (start with
the declaration in coreFeatures.ts), update all imports/uses in adapters, test
helpers, feature interfaces and any exported types or re-exports to the
corrected name, and update related docs/tests to reference coreReactivityFeature
so the change is consistent; ensure you also update any type/interface names,
exported indexes, and usages in runtime code to avoid leftover references to
coreReativityFeature.

In `@packages/table-core/src/core/reactivity/coreReactivityFeature.utils.ts`:
- Around line 20-24: The descriptor for atom.state is being defined without
configurable: true which breaks idempotency of atomToStore; update the code in
the atomToStore path that calls Object.defineProperty(atom, 'state', ...) to
either add configurable: true to the property descriptor (e.g.,
Object.defineProperty(atom, 'state', { get() { return atom.get() },
configurable: true })) or, alternatively, guard by checking
Object.getOwnPropertyDescriptor(atom, 'state') and skip redefining if it already
exists; reference the atom object and the 'state' property in your change so
HMR/tests that re-run atomToStore won’t throw.

In `@packages/table-core/src/core/table/coreTablesFeature.types.ts`:
- Around line 77-83: The runtime selection in constructTable currently prefers
options.state over options.atoms, which contradicts the documented precedence;
update the constructTable logic so for each slice it chooses options.atoms[key]
if present, otherwise options.state[key], and only then falls back to the
internal base atom (keep the existing behavior that library-originating writes
go through the internal base atom and consumers mirror changes via the onXChange
callbacks). Adjust any selection code paths in constructTable (and related
helpers) to reference options.atoms before options.state, and ensure the
ExternalAtoms type, constructTable, and onXChange interactions remain consistent
with this precedence.

---

Outside diff comments:
In `@packages/angular-table/src/helpers/createTableHook.ts`:
- Around line 445-455: The merge in injectTable (injectTable.ts) currently
spreads options()._features after inserting the framework reactivity, which
allows a user-provided _features.coreReativityFeature to override Angular's
angularReactivity(injector); change the merge so Angular's binding wins by
applying the user features first and then setting coreReativityFeature:
angularReactivity(injector) (i.e., merge ...options()._features before, and
explicitly assign coreReativityFeature = angularReactivity(injector) afterwards)
so angularReactivity always takes precedence; reference injectTable,
coreReativityFeature, angularReactivity(injector) and options()._features when
making the change.

In `@packages/svelte-table/src/createTable.svelte.ts`:
- Around line 67-83: The effect only reads mergedOptions.state and
mergedOptions.data so reactive getters like mergedOptions.columns won't become
dependencies; fix by, inside the existing $effect.pre callback that currently
reads state/data (before calling table.setOptions/untrack), iterate over all
keys of mergedOptions and access each value (e.g., for (const k in
mergedOptions) void mergedOptions[k]) so any reactive getters on mergedOptions
(columns, other option props) are tracked and will retrigger the effect that
calls table.setOptions; keep the existing untrack/setOptions placement
unchanged.

---

Duplicate comments:
In `@packages/preact-table/src/reactivity.ts`:
- Around line 17-47: The subscribe wrapper in signalToReadonlyAtom and
signalToWritableAtom overwrites source.subscribe before calling it, causing
infinite recursion; fix by capturing the original subscribe function into a
local variable (e.g., const nativeSubscribe = source.subscribe) before calling
Object.assign, then use nativeSubscribe(observerToCallback(...)) inside the new
subscribe wrapper; keep the rest of the wrapper behavior (observerToCallback
conversion and returning { unsubscribe }) unchanged so semantics match the
original intent.

In `@packages/vue-table/src/reactivity.ts`:
- Around line 60-61: The current untrack implementation in
TableReactivityBindings (untrack: (fn) => fn()) is a no-op and does not suppress
Vue reactive tracking; replace it with a proper tracking-suppression wrapper by
importing pauseTracking and resetTracking from `@vue/reactivity` and invoking
pauseTracking before calling the passed fn and resetTracking in a finally block,
or if you prefer not to use internal APIs, explicitly document the limitation in
the TableReactivityBindings comment and keep untrack as a documented noop;
ensure the change references the untrack symbol so behavior matches the contract
and leave batch unchanged.

---

Nitpick comments:
In `@packages/lit-table/src/TableController.ts`:
- Around line 98-104: The JSDoc in TableController.ts is out of date: it
references constructReactivityFeature while the code now uses
constructReactivityBindings; update the documentation comment to mention
constructReactivityBindings (and any relevant behavior name) so the doc matches
the implementation in the TableController class and surrounding reactivity
integration code.

In `@packages/table-core/src/core/reactivity/coreReactivityFeature.utils.ts`:
- Around line 16-29: The atomToStore function currently mutates the input atom
by casting it to Store<T> and adding a state getter and setState, which pollutes
the original Atom and hides a signature mismatch between Atom.set and
Store.setState; instead create and return a new wrapper object that implements
Store<T>/ReadonlyStore<T> without modifying the original atom: implement a
wrapper with a state getter that calls atom.get(), a setState method that adapts
Atom.set's broader signature to Store.setState by detecting functions vs values
and delegating to atom.set appropriately, and forward other needed
methods/properties to the original atom (referencing atomToStore, Atom.set and
Store.setState to locate the code to change).
🪄 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: 5f3858a4-eace-4cef-9c0a-af69dd03ef34

📥 Commits

Reviewing files that changed from the base of the PR and between 4984c5c and 166bf9f.

📒 Files selected for processing (72)
  • examples/preact/basic-external-atoms/index.html
  • examples/preact/basic-external-state/index.html
  • examples/preact/basic-use-app-table/index.html
  • examples/preact/basic-use-table/index.html
  • examples/preact/column-groups/index.html
  • examples/preact/column-ordering/index.html
  • examples/preact/column-pinning-split/index.html
  • examples/preact/column-pinning-sticky/index.html
  • examples/preact/column-pinning/index.html
  • examples/preact/column-resizing-performant/index.html
  • examples/preact/column-resizing/index.html
  • examples/preact/column-sizing/index.html
  • examples/preact/column-visibility/index.html
  • examples/preact/composable-tables/index.html
  • examples/preact/composable-tables/src/components/header-components.tsx
  • examples/preact/custom-plugin/index.html
  • examples/preact/custom-plugin/src/main.tsx
  • examples/preact/expanding/index.html
  • examples/preact/expanding/src/main.tsx
  • examples/preact/filters-faceted/index.html
  • examples/preact/filters-faceted/src/main.tsx
  • examples/preact/filters-fuzzy/index.html
  • examples/preact/filters-fuzzy/src/main.tsx
  • examples/preact/filters/index.html
  • examples/preact/filters/src/main.tsx
  • examples/preact/grouping/index.html
  • examples/preact/pagination/index.html
  • examples/preact/pagination/src/main.tsx
  • examples/preact/row-pinning/index.html
  • examples/preact/row-pinning/src/main.tsx
  • examples/preact/row-selection/index.html
  • examples/preact/row-selection/src/main.tsx
  • examples/preact/sorting/index.html
  • examples/preact/sub-components/index.html
  • examples/preact/with-tanstack-query/index.html
  • packages/angular-table/src/helpers/createTableHook.ts
  • packages/angular-table/src/injectTable.ts
  • packages/angular-table/src/reactivity.ts
  • packages/lit-table/src/TableController.ts
  • packages/lit-table/tests/unit/defaultReactivity.test.ts
  • packages/preact-table/src/reactivity.ts
  • packages/preact-table/src/useTable.ts
  • packages/preact-table/tests/unit/signals.test.ts
  • packages/react-table/src/useTable.ts
  • packages/solid-table/src/createTable.ts
  • packages/solid-table/src/reactivity.ts
  • packages/svelte-table/src/createTable.svelte.ts
  • packages/svelte-table/src/reactivity.svelte.ts
  • packages/table-core/package.json
  • packages/table-core/src/core/coreFeatures.ts
  • packages/table-core/src/core/reactivity/constructReactivityBindings.ts
  • packages/table-core/src/core/reactivity/coreReactivityFeature.types.ts
  • packages/table-core/src/core/reactivity/coreReactivityFeature.utils.ts
  • 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/index.ts
  • packages/table-core/src/reactivity.ts
  • packages/table-core/src/types/TableFeatures.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/reactivity.ts
  • packages/vue-table/src/useTable.ts
  • packages/vue-table/tests/unit/signals.test.ts
💤 Files with no reviewable changes (27)
  • examples/preact/basic-use-table/index.html
  • examples/preact/column-resizing-performant/index.html
  • examples/preact/filters-fuzzy/index.html
  • examples/preact/column-ordering/index.html
  • examples/preact/basic-external-atoms/index.html
  • examples/preact/expanding/index.html
  • examples/preact/sorting/index.html
  • examples/preact/basic-use-app-table/index.html
  • examples/preact/filters/index.html
  • examples/preact/row-selection/index.html
  • examples/preact/custom-plugin/index.html
  • examples/preact/filters-faceted/index.html
  • examples/preact/basic-external-state/index.html
  • examples/preact/column-groups/index.html
  • examples/preact/column-pinning-sticky/index.html
  • examples/preact/with-tanstack-query/index.html
  • examples/preact/column-sizing/index.html
  • examples/preact/column-visibility/index.html
  • examples/preact/composable-tables/index.html
  • examples/preact/row-pinning/index.html
  • examples/preact/column-pinning/index.html
  • examples/preact/sub-components/index.html
  • examples/preact/pagination/index.html
  • examples/preact/column-pinning-split/index.html
  • examples/preact/column-resizing/index.html
  • examples/preact/grouping/index.html
  • packages/table-core/src/index.ts
✅ Files skipped from review due to trivial changes (7)
  • packages/preact-table/tests/unit/signals.test.ts
  • packages/table-core/tsdown.config.ts
  • packages/table-core/src/reactivity.ts
  • packages/table-core/src/core/table/coreTablesFeature.utils.ts
  • packages/table-core/src/types/TableFeatures.ts
  • packages/vue-table/tests/unit/signals.test.ts
  • packages/table-core/tests/unit/core/columns/constructColumn.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/lit-table/tests/unit/defaultReactivity.test.ts

Comment on lines +59 to +65
* Creates a computed that subscribe to changes in the table store with a custom selector.
* Default equality function is "shallow".
*/
computed: AngularTableComputed<TFeatures>
Subscribe: AngularTableComputed<TFeatures>
computed: <TSubSelected = {}>(props: {
selector: (state: TableState<TFeatures>) => TSubSelected
equal?: ValueEqualityFn<TSubSelected>
}) => Signal<Readonly<TSubSelected>>
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 the exported AngularTableComputed type in sync with the narrowed API.

Lines 59-65 make table.computed selector-only, but the exported AngularTableComputed interface above still exposes the removed source overloads. That leaves a public type compiling against an API this implementation no longer supports. Please narrow or remove AngularTableComputed in the same change.

Also applies to: 163-169

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

In `@packages/angular-table/src/injectTable.ts` around lines 59 - 65, The exported
AngularTableComputed type is out of sync with the new table.computed
implementation (which now only accepts a selector + optional equal), so update
or remove AngularTableComputed to match the narrowed API: remove the old
"source" overloads and ensure the type signature exactly matches computed:
<TSubSelected = {}>(props: { selector: (state: TableState<TFeatures>) =>
TSubSelected; equal?: ValueEqualityFn<TSubSelected> }) =>
Signal<Readonly<TSubSelected>> (also apply the same narrowing for the
duplicate/related type declared near lines 163-169), referencing the
AngularTableComputed identifier and any duplicate interface to keep public types
consistent with table.computed.

Comment on lines +137 to +143
const table = constructTable({
...options(),
_features: {
coreReativityFeature: angularReactivity(injector),
...options()._features,
angularReactivityFeature,
},
}

const table = constructTable(resolvedOptions) as AngularTable<
TFeatures,
TData,
TSelected
>
const tableState = injectSelector(table.store, (state) => state, {
injector,
})
const tableOptions = injectSelector(table.optionsStore, (state) => state, {
injector,
})

const updatedOptions = computed<TableOptions<TFeatures, TData>>(() => {
const tableOptionsValue = options()
const result: TableOptions<TFeatures, TData> = {
...untracked(() => table.options),
...tableOptionsValue,
_features: { ...tableOptionsValue._features, angularReactivityFeature },
}
if (tableOptionsValue.state) {
result.state = tableOptionsValue.state
}
return result
})

effect(
() => {
const newOptions = updatedOptions()
untracked(() => table.setOptions(newOptions))
},
{ injector, debugName: 'tableOptionsUpdate' },
)
}) as AngularTable<TFeatures, TData, TSelected>
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

Don't let caller features override the Angular reactivity binding.

Because ...options()._features is spread after coreReativityFeature: angularReactivity(injector), any caller-provided coreReativityFeature wins. In that case the table can stop being backed by Angular signals, and table.state / table.computed / table.value will stop updating reactively. Cache options() once and write the Angular binding last.

Suggested fix
-    const table = constructTable({
-      ...options(),
-      _features: {
-        coreReativityFeature: angularReactivity(injector),
-        ...options()._features,
-      },
-    }) as AngularTable<TFeatures, TData, TSelected>
+    const initialOptions = options()
+    const table = constructTable({
+      ...initialOptions,
+      _features: {
+        ...initialOptions._features,
+        coreReativityFeature: angularReactivity(injector),
+      },
+    }) as AngularTable<TFeatures, TData, TSelected>
📝 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 table = constructTable({
...options(),
_features: {
coreReativityFeature: angularReactivity(injector),
...options()._features,
angularReactivityFeature,
},
}
const table = constructTable(resolvedOptions) as AngularTable<
TFeatures,
TData,
TSelected
>
const tableState = injectSelector(table.store, (state) => state, {
injector,
})
const tableOptions = injectSelector(table.optionsStore, (state) => state, {
injector,
})
const updatedOptions = computed<TableOptions<TFeatures, TData>>(() => {
const tableOptionsValue = options()
const result: TableOptions<TFeatures, TData> = {
...untracked(() => table.options),
...tableOptionsValue,
_features: { ...tableOptionsValue._features, angularReactivityFeature },
}
if (tableOptionsValue.state) {
result.state = tableOptionsValue.state
}
return result
})
effect(
() => {
const newOptions = updatedOptions()
untracked(() => table.setOptions(newOptions))
},
{ injector, debugName: 'tableOptionsUpdate' },
)
}) as AngularTable<TFeatures, TData, TSelected>
const initialOptions = options()
const table = constructTable({
...initialOptions,
_features: {
...initialOptions._features,
coreReativityFeature: angularReactivity(injector),
},
}) as AngularTable<TFeatures, TData, TSelected>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-table/src/injectTable.ts` around lines 137 - 143, Cache the
result of options() into a local variable, then pass that cached options to
constructTable while merging _features so that the Angular reactivity binding
overrides any caller-provided feature: take the cachedOptions._features (if any)
and spread them first, then set coreReativityFeature:
angularReactivity(injector) last so it wins; update the constructTable call (and
the cast to AngularTable<TFeatures, TData, TSelected>) to use cachedOptions and
the reordered _features merge to ensure table.state/table.computed/table.value
remain backed by Angular signals.

@@ -0,0 +1,63 @@
// TOTO - re-explore preact signals for 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 | 🟡 Minor | ⚡ Quick win

Typo: "TOTO" should be "TODO".

📝 Proposed fix
-// TOTO - re-explore preact signals for reactivity
+// TODO - re-explore preact signals for reactivity
📝 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
// TOTO - re-explore preact signals for reactivity
// TODO - re-explore preact signals for reactivity
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/preact-table/src/reactivity.ts` at line 1, Fix the typo in the
top-of-file comment: change the comment text "TOTO - re-explore preact signals
for reactivity" to "TODO - re-explore preact signals for reactivity" so the
intent is clear and searchable; locate the string "TOTO - re-explore preact
signals for reactivity" in the file and update it to "TODO..." accordingly.

Comment on lines +124 to +135
const state = useSelector(table.store, selector, { compare: shallow })
const options = useSelector(table.optionsStore, (options) => options, {
compare: shallow,
})

return useMemo(
() => ({
...table,
options,
state,
}),
[state, table],
[table, options, 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

The returned state no longer matches PreactTable['state'].

This hook now returns the raw selector result, but PreactTable.state is still declared to always include columns and data. That leaves existing consumers able to compile against table.state.columns while getting undefined at runtime. Either restore those fields on the returned state or update the public type/docs as a breaking change.

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

In `@packages/preact-table/src/useTable.ts` around lines 124 - 135, The hook
useTable now returns the raw selector result in state which can omit required
fields (columns, data) from the declared PreactTable['state'], causing runtime
undefined for table.state.columns; fix by ensuring the returned state always
contains the required shape: when building the return object in useTable (the
selector/result used by useSelector and the returned state), normalize or merge
missing fields so state.columns and state.data are present (e.g., fall back to
table.state.columns/table.state.data or default empty arrays/objects) or update
the public type to reflect the new optional fields — locate useTable, the
selector passed into useSelector, and the returned object spreading table to
implement the normalization or update the type accordingly.

Comment on lines 152 to +165
const state = useSelector(table.store, selector, { compare: shallow })
const options = useSelector(table.optionsStore, (options) => options, {
compare: shallow,
})

// we know this is not the most efficient way to return the table,
// but it is required for the react compiler to work
return useMemo(
() => ({
...table,
options: tableOptions,
options,
state,
}),
[table, tableOptions, state],
[table, options, 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

ReactTable['state'] is now unsound at runtime.

The hook returns the raw selector output, but the public ReactTable.state type still promises columns and data. Consumers can keep compiling against table.state.columns and now read undefined at runtime. Either re-add those fields to the returned state or update the public API/migration surface explicitly.

🤖 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 152 - 165, The returned
object from useTable (in useTable.ts where useSelector and useMemo are used to
return {...table, options, state}) can expose a runtime-missing `columns` and
`data` despite the public ReactTable.state type; fix this by
normalizing/ensuring the returned state includes the expected keys (e.g.,
guarantee state.columns and state.data exist by merging defaults or falling back
to table.initialState or empty arrays) before returning from useMemo, or
alternatively update the public API types and migration notes if you intend to
remove those fields (adjust the return in useMemo and the selector usage
accordingly).

import type { TableReactivityBindings } from '../reactivity'

export interface CoreFeatures {
coreReativityFeature?: 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 | 🟠 Major | ⚡ Quick win

coreReativityFeature? is marked optional but constructTable accesses it with !, creating a silent runtime failure.

coreFeatures (the exported object) does not include coreReativityFeature, so any user who passes only { ...coreFeatures } to _features will have _reactivity = undefined inside constructTable, causing downstream failures with no type-system warning. The ? and the ! are contradictory: the interface promises "this is fine to omit" while the implementation assumes "this is always present".

Fix options:

  1. Make it required in the interface and include a default in coreFeatures, or
  2. Keep it optional and add a guard in constructTable.ts:
// in constructTable.ts
const _reactivity = tableOptions._features.coreReativityFeature
if (!_reactivity) {
  throw new Error('[table-core] coreReativityFeature must be provided in _features')
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/src/core/coreFeatures.ts` at line 10, The interface marks
coreReativityFeature as optional but constructTable accesses it with non-null
assertion, causing silent runtime failures; fix by either (A) making
coreReativityFeature required in the interface and adding a default
coreReativityFeature implementation to the exported coreFeatures object so
callers can safely spread `{ ...coreFeatures }`, or (B) keep it optional and add
a runtime guard in constructTable: read
tableOptions._features.coreReativityFeature into _reactivity and if falsy throw
a clear Error like '[table-core] coreReativityFeature must be provided in
_features' before any use; update references to the symbols
coreReativityFeature, coreFeatures, constructTable, _features, and _reactivity
accordingly.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Typo in new public API: coreReativityFeaturecoreReactivityFeature.

"Reativity" is missing the letter c. This name is propagated across every framework adapter, test helper, and feature interface in this PR. Fixing it now (alpha branch) is a one-shot rename; after a stable release it becomes a breaking change across all consumers.

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

In `@packages/table-core/src/core/coreFeatures.ts` at line 10, The public API
field is misspelled: rename the symbol coreReativityFeature to
coreReactivityFeature across the codebase (start with the declaration in
coreFeatures.ts), update all imports/uses in adapters, test helpers, feature
interfaces and any exported types or re-exports to the corrected name, and
update related docs/tests to reference coreReactivityFeature so the change is
consistent; ensure you also update any type/interface names, exported indexes,
and usages in runtime code to avoid leftover references to coreReativityFeature.

Comment on lines +20 to +24
Object.defineProperty(atom, 'state', {
get() {
return atom.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 | 🟠 Major | ⚡ Quick win

Object.defineProperty without configurable: true makes atomToStore non-idempotent.

With configurable defaulting to false, calling atomToStore a second time on the same atom instance throws TypeError: Cannot redefine property: state. This can realistically occur in HMR scenarios, tests that reuse atom instances, or any framework that re-initializes the table without recreating the atom.

🛡️ Proposed fix
-  Object.defineProperty(atom, 'state', {
+  Object.defineProperty(atom, 'state', {
+    configurable: true,
+    enumerable: true,
     get() {
       return atom.get()
     },
   })

Alternatively, guard with a check first:

+  if (!Object.getOwnPropertyDescriptor(atom, 'state')) {
     Object.defineProperty(atom, 'state', {
+      configurable: true,
       get() { return atom.get() },
     })
+  }
📝 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
Object.defineProperty(atom, 'state', {
get() {
return atom.get()
},
})
Object.defineProperty(atom, 'state', {
configurable: true,
enumerable: true,
get() {
return atom.get()
},
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/table-core/src/core/reactivity/coreReactivityFeature.utils.ts`
around lines 20 - 24, The descriptor for atom.state is being defined without
configurable: true which breaks idempotency of atomToStore; update the code in
the atomToStore path that calls Object.defineProperty(atom, 'state', ...) to
either add configurable: true to the property descriptor (e.g.,
Object.defineProperty(atom, 'state', { get() { return atom.get() },
configurable: true })) or, alternatively, guard by checking
Object.getOwnPropertyDescriptor(atom, 'state') and skip redefining if it already
exists; reference the atom object and the 'state' property in your change so
HMR/tests that re-run atomToStore won’t throw.

Comment on lines 77 to +83
* Optionally, provide your own external writable atoms for individual state slices.
* When an atom is provided for a given slice, it takes precedence over `options.state[key]`
* and the internal base atom for that slice. Writes originating from the library are
* still routed through the internal base atom; consumers are responsible for
* mirroring changes back to their external atom via the corresponding `onXChange` callback.
*/
atoms?: ExternalAtoms<TFeatures>
readonly atoms?: ExternalAtoms<TFeatures>
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

Documented atom precedence doesn't match the runtime order.

Lines 77-83 and Lines 151-155 say options.atoms[key] wins over options.state[key], but packages/table-core/src/core/table/constructTable.ts:61-87 currently reads state first and only falls back to the external atom afterward. If both are present, the external atom never owns that slice, which breaks the contract this PR is introducing. Please align the runtime order or these docs/types before merging.

Also applies to: 151-155

🤖 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
77 - 83, The runtime selection in constructTable currently prefers options.state
over options.atoms, which contradicts the documented precedence; update the
constructTable logic so for each slice it chooses options.atoms[key] if present,
otherwise options.state[key], and only then falls back to the internal base atom
(keep the existing behavior that library-originating writes go through the
internal base atom and consumers mirror changes via the onXChange callbacks).
Adjust any selection code paths in constructTable (and related helpers) to
reference options.atoms before options.state, and ensure the ExternalAtoms type,
constructTable, and onXChange interactions remain consistent with this
precedence.

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: 1

🤖 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/react-table/src/useLegacyTable.ts`:
- Around line 462-465: The inline comment for getState is stale — it says "all
state except for columns and data" but getState (defined via useCallback
returning table.state) now returns the full table.state; update or remove that
comment to accurately reflect the implementation: either change it to something
like "return full table.state" or delete the comment near the getState
useCallback so the comment and the returned value (table.state) are consistent.
🪄 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: fa3ce0cc-ef46-48a0-bed5-1aee661e8414

📥 Commits

Reviewing files that changed from the base of the PR and between 166bf9f and ae7eb1f.

📒 Files selected for processing (1)
  • packages/react-table/src/useLegacyTable.ts

Comment thread packages/react-table/src/useLegacyTable.ts
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: 1

🤖 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/react-table/src/useLegacyTable.ts`:
- Around line 405-452: Add a migration caveat to
docs/framework/react/guide/use-legacy-table.md explaining that the lazy
initializer inside useLegacyTable (the const [_rowModels] = useState(() => { ...
}) block) captures row model options (e.g., getFilteredRowModel and custom
filterFns, sortFns, aggregationFns) only on first render and does not update on
rerenders; state that toggling row model features or replacing these custom
functions after mount will have no effect and the table must be remounted to
pick up changes, with a brief example or note advising users to remount to
change row model behavior.
🪄 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: 870519e2-2145-4538-9b21-629dea31ba4c

📥 Commits

Reviewing files that changed from the base of the PR and between ae7eb1f and 781e5bc.

📒 Files selected for processing (1)
  • packages/react-table/src/useLegacyTable.ts

Comment on lines +405 to +452
const [_rowModels] = useState(() => {
const rowModels: CreateRowModels_All<StockFeatures, TData> = {}

// Legacy row model options are setup-only. Capture the first render's
// marker options to match the table instance lifecycle.

if (getFilteredRowModel) {
rowModels.filteredRowModel = createFilteredRowModel({
...filterFns,
...options.filterFns,
})
}

if (getSortedRowModel) {
rowModels.sortedRowModel = createSortedRowModel({
...sortFns,
...options.sortFns,
})
}

if (getPaginationRowModel) {
rowModels.paginatedRowModel = createPaginatedRowModel()
}

if (getExpandedRowModel) {
rowModels.expandedRowModel = createExpandedRowModel()
}

if (getGroupedRowModel) {
rowModels.groupedRowModel = createGroupedRowModel({
...aggregationFns,
...options.aggregationFns,
})
}

if (getFacetedRowModel) {
rowModels.facetedRowModel = createFacetedRowModel()
}

if (getFacetedMinMaxValues) {
rowModels.facetedMinMaxValues = createFacetedMinMaxValues()
}

if (getFacetedUniqueValues) {
rowModels.facetedUniqueValues = createFacetedUniqueValues()
}

return rowModels
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:

# Get the file size and read the relevant section
wc -l packages/react-table/src/useLegacyTable.ts

Repository: TanStack/table

Length of output: 105


🏁 Script executed:

# Read the file around the flagged lines and surrounding context
sed -n '390,480p' packages/react-table/src/useLegacyTable.ts

Repository: TanStack/table

Length of output: 2344


🏁 Script executed:

# Search for all usages of _rowModels to understand how it's used
rg '_rowModels' packages/react-table/src/useLegacyTable.ts -n -B 2 -A 2

Repository: TanStack/table

Length of output: 4085


🏁 Script executed:

# Check the full function to understand the lifecycle and dependencies
sed -n '1,50p' packages/react-table/src/useLegacyTable.ts

Repository: TanStack/table

Length of output: 1379


🏁 Script executed:

# Look for any dependency arrays or effect hooks that might update row models
rg 'useEffect|useMemo|useCallback' packages/react-table/src/useLegacyTable.ts -n -A 3

Repository: TanStack/table

Length of output: 642


🏁 Script executed:

# Search for test files related to useLegacyTable
fd -t f 'useLegacyTable' --type f -e 'test.ts' -e 'spec.ts' -e 'test.tsx' -e 'spec.tsx'

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Look for any tests or documentation about legacy table behavior
rg 'useLegacyTable' -t ts -t tsx -l

Repository: TanStack/table

Length of output: 86


🏁 Script executed:

# Check if there are any other comments or documentation about the setup-only behavior
rg 'setup-only|non-reactive|reactive' packages/react-table/src/ -i -n -B 2 -A 2

Repository: TanStack/table

Length of output: 499


🏁 Script executed:

# Look for any mechanism that might update _rowModels after initialization
rg 'setRowModels|updateRowModels|_rowModels.*=' packages/react-table/src/useLegacyTable.ts -n

Repository: TanStack/table

Length of output: 102


🏁 Script executed:

# Check if filterFns, sortFns, aggregationFns are used elsewhere after the initializer
sed -n '1,488p' packages/react-table/src/useLegacyTable.ts | grep -n 'filterFns\|sortFns\|aggregationFns'

Repository: TanStack/table

Length of output: 1418


🏁 Script executed:

# Search for migration guides or documentation about this limitation
rg 'useLegacyTable|setup-only|non-reactive' -t md -t txt -i --max-count=20

Repository: TanStack/table

Length of output: 12607


🏁 Script executed:

# Look for CHANGELOG or migration guides
fd -t f '(CHANGELOG|MIGRATION|GUIDE)' --type f

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Check if there are any GitHub issues or discussions referenced
rg 'TODO|FIXME|XXX' packages/react-table/src/useLegacyTable.ts -n

Repository: TanStack/table

Length of output: 40


🏁 Script executed:

# Get full context around the setUp-only comment and any surrounding documentation
sed -n '350,470p' packages/react-table/src/useLegacyTable.ts

Repository: TanStack/table

Length of output: 3352


🏁 Script executed:

# Read the full migration guide for useLegacyTable
cat docs/framework/react/guide/use-legacy-table.md

Repository: TanStack/table

Length of output: 7288


Row model initialization is non-reactive—document this caveat for v8 migrations.

The lazy useState(() => ...) initializer captures row model options (getFilteredRowModel, custom filterFns/sortFns/aggregationFns, etc.) on first render and never updates them. While the code comment indicates this is intentional ("setup-only"), this is not documented in the migration guide as a caveat. Users upgrading from v8—where these options are reactive across rerenders—may silently encounter bugs if they attempt to swap custom functions or toggle row model features after mount. Add a caveats section to docs/framework/react/guide/use-legacy-table.md explicitly stating that row model options are frozen at initialization and cannot be changed without remounting the table.

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

In `@packages/react-table/src/useLegacyTable.ts` around lines 405 - 452, Add a
migration caveat to docs/framework/react/guide/use-legacy-table.md explaining
that the lazy initializer inside useLegacyTable (the const [_rowModels] =
useState(() => { ... }) block) captures row model options (e.g.,
getFilteredRowModel and custom filterFns, sortFns, aggregationFns) only on first
render and does not update on rerenders; state that toggling row model features
or replacing these custom functions after mount will have no effect and the
table must be remounted to pick up changes, with a brief example or note
advising users to remount to change row model behavior.

@KevinVandy KevinVandy merged commit 753baba into alpha May 4, 2026
5 checks passed
@KevinVandy KevinVandy deleted the feat/native-reactivity branch May 4, 2026 15:29
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