Skip to content

Commit 4a5fcbe

Browse files
KevinVandyclaudeautofix-ci[bot]
authored
refactor: move render-phase sync strategy into core reactivity bindings (#6459)
* refactor: move render-phase sync strategy into core reactivity bindings Builds on the deferred controlled-state publication from this branch and consolidates it behind a consistent adapter interface: - TableReactivityBindings gains two optional properties describing the two strategy axes: deferExternalStateSync (write-side: is setOptions a notification-safe moment?) and commit (invalidation hook for readonly atoms whose compute reads non-reactive plain options). constructTable warns in dev when defer is set with plain options but no commit hook. - table_setOptions consults the bindings flag instead of a per-call { syncExternalState } option, so an adapter's strategy is declared once. - table_syncExternalStateToBaseAtoms replaces the arguments.length overloads with an explicit `capturedState | null` sentinel, and now owns the commit bump: it runs inside the write batch and fires even when nothing is published, so ownership releases still invalidate subscribers. - New renderPhaseReactivity preset in @tanstack/table-core/reactivity hosts the live readonly-atom facade + commit atom (moved from the React adapter). Store primitives are injected by the adapter so all atoms share one store instance with user external atoms. Preact can adopt the same preset later. - New createCommitFilteredSource replaces useTableSelector: because facade snapshots are referentially stable, a reference check on the last snapshot read is enough to skip the root hook's redundant post-commit notification. This drops the committed-selection refs and the implicit requirement that the selector's layout effect run before the publish effect. - useTable: plain useSelector over the filtered source; the publish layout effect is a single table_syncExternalStateToBaseAtoms call. All 11 react-table tests from this branch pass unchanged. table-core: 1017 tests, types, lint, size-limit green. Both example e2e smoke suites pass. Verified in basic-external-state (StrictMode + React Compiler + devtools): one render pass and one commit per controlled update, no render-phase warning, correct behavior through pagination/sorting/page-size/1M-row stress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adopt render-phase reactivity preset in the preact adapter Preact had the identical sync-options-during-render code as React and paid the same cost silently: the mid-render store notification scheduled an extra deferred render per controlled update (Preact never warns about it). Same shape as the React adapter: preactReactivity collapses onto renderPhaseReactivity with @tanstack/preact-store primitives, useTable defers publication to an isomorphic layout effect with the captured controlled state, and the root useSelector reads through createCommitFilteredSource. New unit tests assert one render pass per controlled update with consistent controlled/selected/atom/store/row-model reads, exactly one post-commit store notification for external subscribers, and uncontrolled updates still re-rendering. New e2e spec exercises controlled pagination in the basic-external-state preact example. Types, lint, and build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adopt render-phase reactivity in the lit adapter, un-pin subscribe islands from stale renders The lit basic-subscribe e2e regression had two stacked causes: 1. The subscribe directive returned noChange for host-driven re-renders with an unchanged source + selector, so islands only ever updated from store notifications. They previously received one for ANY setOptions call because the aggregate state snapshot was rebuilt without a comparator; the (correct) shallow compare on table.store stopped notifying for options-only changes, pinning islands to stale row models after Regenerate Data. The directive now re-renders islands on every host update and adopts the latest template closure, while subscriptions still drive updates between host renders. 2. Lit was the off-diagonal reactivity case: a reactive options store written during render(), costing a second update cycle per interaction (requestUpdate mid-update from the optionsStore subscription). litReactivity now uses the renderPhaseReactivity preset with @tanstack/lit-store primitives: plain options synced during render, captured controlled state published from hostUpdated(), and the controller's root subscription reads through createCommitFilteredSource. The optionsStore subscription is gone; options changes flow through host renders (lit reactive properties), which 30 of 31 lit examples already use. basic-subscribe was the lone outlier constructing the table once and pushing data via imperative setOptions in updated() — it now re-syncs options per render pass like the rest. Measured on basic-subscribe: Regenerate Data works again with one host update (previously two, with stale islands), zero idle update churn, and a row-selection click still updates only its island with zero host updates. Unit tests: directive/controller suites updated to the new contract plus a hostUpdated publication test (6/6). All 34 lit example e2e suites pass, including the previously failing basic-subscribe. Types, lint, build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: drop dev-warning assertion to match removed bindings coherence warning The constructTable dev warning for deferExternalStateSync-without-commit was removed in the previous commit; align the test suite (1010 tests green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: apply automated fixes * consolidate constructTable features loop again * fix: keep the two-phase feature lifecycle in the consolidated features loop The consolidated loop interleaved constructTableAPIs with initTableInstanceData per feature (and ran APIs first within each feature), breaking the lifecycle contract from #6446 that constructTable.test.ts encodes: every feature's table instance data initializes before ANY table APIs are constructed, so API constructors can read instance data owned by other features and init hooks observe a pre-API table. Keep the consolidation of the init-fn collection with instance-data init (one phase-1 loop writing the precomputed arrays directly), and restore constructTableAPIs as its own second pass. table-core: 1097/1097. React/preact/lit adapter suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor!: process feature lifecycle hooks in a single pass Drop the two-phase construction contract (all initTableInstanceData before any constructTableAPIs). Every in-repo hook is pure property assignment with lazy API closures, so the phase barrier bought nothing; features are now processed in one registration-order pass with each feature's instance data initialized just before its own APIs are constructed. New contract, encoded in the lifecycle test and documented in the custom-features guides and TableFeature jsdoc: hooks may rely on data and APIs of features registered earlier; eager cross-feature reads belong in lazy API bodies. table-core 1097/1097; react/preact/lit adapter suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: require precomputed instance-init fn arrays on the table type constructTable always assigns the five _*InstanceInitFns arrays before any constructor can run, so model them as required and drop the non-null assertions at the use sites (including the leftover one in buildHeaderGroups that eslint flagged as unnecessary). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent a0bbd59 commit 4a5fcbe

35 files changed

Lines changed: 932 additions & 480 deletions

docs/framework/alpine/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
140140

141141
#### initTableInstanceData and resetTableInstanceData
142142

143-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
143+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
144144

145145
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
146146

docs/framework/angular/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
146146

147147
##### initTableInstanceData and resetTableInstanceData
148148

149-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
149+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
150150

151151
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
152152

docs/framework/ember/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
146146

147147
##### initTableInstanceData and resetTableInstanceData
148148

149-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
149+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
150150

151151
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
152152

docs/framework/lit/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
138138

139139
#### initTableInstanceData and resetTableInstanceData
140140

141-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
141+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
142142

143143
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
144144

docs/framework/preact/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
146146

147147
##### initTableInstanceData and resetTableInstanceData
148148

149-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
149+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
150150

151151
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
152152

docs/framework/react/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
146146

147147
##### initTableInstanceData and resetTableInstanceData
148148

149-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
149+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
150150

151151
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
152152

docs/framework/solid/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
140140

141141
#### initTableInstanceData and resetTableInstanceData
142142

143-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
143+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
144144

145145
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
146146

docs/framework/svelte/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
140140

141141
#### initTableInstanceData and resetTableInstanceData
142142

143-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
143+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
144144

145145
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
146146

docs/framework/vue/guide/custom-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d
140140

141141
#### initTableInstanceData and resetTableInstanceData
142142

143-
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
143+
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.
144144

145145
Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.
146146

examples/lit/basic-subscribe/src/main.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,25 @@ class LitTableExample extends LitElement {
118118

119119
private tableController = new TableController<typeof features, Person>(this)
120120

121-
private table = this.tableController.table(
122-
{
121+
// Options are re-synced on every render pass (like the other examples), so
122+
// reactive inputs such as `this._data` flow into the table in the same
123+
// update that renders them.
124+
private tableOptions() {
125+
return {
123126
features,
124127
columns,
125128
data: this._data,
126-
getRowId: (row) => row.id,
129+
getRowId: (row: Person) => row.id,
127130
enableRowSelection: true,
128131
atoms: {
129132
rowSelection: rowSelectionAtom,
130133
},
131134
debugTable: true,
132-
},
135+
}
136+
}
137+
138+
private table = this.tableController.table(
139+
this.tableOptions(),
133140
() => null, // subscribe to no table state by default; use table.subscribe below
134141
)
135142

@@ -141,12 +148,6 @@ class LitTableExample extends LitElement {
141148
pagination: state.pagination,
142149
})
143150

144-
protected updated(changedProperties: Map<string, unknown>) {
145-
if (changedProperties.has('_data')) {
146-
this.table.setOptions((prev) => ({ ...prev, data: this._data }))
147-
}
148-
}
149-
150151
private renderColumnFilter(
151152
context: HeaderContext<typeof features, Person, unknown>,
152153
) {
@@ -211,6 +212,8 @@ class LitTableExample extends LitElement {
211212
}
212213

213214
protected render() {
215+
this.table = this.tableController.table(this.tableOptions(), () => null)
216+
214217
return html`
215218
<div class="demo-root">
216219
<div>

0 commit comments

Comments
 (0)