refactor: move render-phase sync strategy into core reactivity bindings - #6459
refactor: move render-phase sync strategy into core reactivity bindings#6459KevinVandy wants to merge 1 commit into
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit e4b3f56
☁️ Nx Cloud last updated this comment at |
Builds directly on this branch's deferred controlled-state publication (great work — the deferred publish, read-through atoms, live facade, and captured-state semantics are all load-bearing and verified). This PR keeps all of that and consolidates it behind a consistent adapter interface, while simplifying the two pieces that turned out to be replaceable with less machinery. All 11
useTabletests from this branch pass unchanged.What changed
1. Strategy flags on
TableReactivityBindings(instead of a per-call option)deferExternalStateSync?: booleanandcommit?: () => voidjoincreateOptionsStore/wrapExternalAtoms. They describe two independent axes we confirmed across all 8 adapters:createOptionsStore): can the dependency graph see options changes? Signal adapters: yes. React/Preact: no — hence the live facade.deferExternalStateSync): issetOptionsa notification-safe moment? Signal adapters sync inside effects/watchers (eager stays correct — unchanged). React/Preact sync during render (defer).table_setOptionsconsults the flag, so an adapter declares its strategy once instead of threading{ syncExternalState: false }through call sites.constructTablewarns in dev whendeferExternalStateSyncis set with plain options but nocommithook (the one combination that silently strands subscribers).2. Core owns the commit bump
table_syncExternalStateToBaseAtoms(table, capturedState | null, compare?)replaces thearguments.lengthoverloads with an explicit sentinel (null= captured "no controlled state", omitted = read current options), and now calls_reactivity.commit?.()inside its own batch — including when nothing is published. The ownership-release invariant ("bump even with no atom write") was the easiest thing for a future adapter to forget, so it lives in core now. The adapter's layout effect is a single call.3.
renderPhaseReactivitypreset in@tanstack/table-core/reactivityThe live readonly-atom facade + commit atom move from
react-table/src/reactivity.tsinto a core preset. Store primitives (createAtom,batch) are injected by the adapter so every atom lives in the adapter's store instance alongside user external atoms (dependency tracking is module-global — the preset must not use table-core's own store copy). The React reactivity file is now ~15 lines, and the Preact port becomes a mechanical follow-up.4.
createCommitFilteredSourcereplacesuseTableSelectorThe root-notification filter is real and necessary — without it the owner re-renders once per controlled update just to find nothing changed. But because the facade guarantees referentially stable snapshots, a reference check on the last snapshot the hook read achieves the same filtering as the committed-selection bookkeeping, without the selector refs and without the implicit requirement that
useTableSelector's layout effect run before the publish effect.useTablenow uses plainuseSelectorover the filtered source.Verification
react-table: 11/11 tests from this branch, unchanged + types + linttable-core: 1017/1017 (incl. 6 new tests for the flag, sentinel, commit-on-empty-publish, dev warning, and filter) + types + lint + size-limit (19.53 kB / 25 kB)basic-external-state3/3,basic-external-atoms3/3 (Chromium)basic-external-state(StrictMode + React Compiler + devtools): one render pass and one commit per controlled update, zero render-phase warnings, correct behavior through pagination / sorting (with auto page reset) / page-size / 1M-row stress. Baselinebetapays two render passes per controlled update (the warning marks a discarded pass), so this lands at half the render work.Not in this PR (follow-ups)
renderPhaseReactivity(it has the identical sync-on-render code today, silently paying the double render+commit)controller.table()writes its options store duringrender();hostUpdated()is the natural commit hook)atoms > options.state > baseAtomread-through inuse-table.tswith core's (core'shasOwncheck also distinguishes explicitly-undefined controlled slices)🤖 Generated with Claude Code