refactor(bindx-react): stable EntityHandle identity (fine-grained reactivity)#56
Open
matej21 wants to merge 1 commit into
Open
refactor(bindx-react): stable EntityHandle identity (fine-grained reactivity)#56matej21 wants to merge 1 commit into
matej21 wants to merge 1 commit into
Conversation
…a changes EntityHandle is a stateless live view over the store, yet `useEntity` and `<Entity>` recreated it on every snapshot/version change (by feeding `snapshot`/`version` into the handle's useMemo deps) purely to hand memoized children a fresh reference. That defeated the store's fine-grained reactivity: any field change threw away the whole handle subtree and its caches, re-rendered the entire entity subtree, churned `useEffect([handle])` deps, and created the "superseded handle" concept the (now-removed) dispose lifecycle existed to clean up. The blessed leaf components (`<Field>`, `<HasOne>`, `<HasMany>`) already subscribe to the store themselves (useField/useAccessor -> useSyncExternalStore), so they re-render on data changes without needing a changing handle reference. Drop `snapshot`/`version` from the handle memo deps so the handle keeps a stable identity. The host still subscribes and re-renders (keeping inline `.value` reads in `children` fresh); memoized leaves now re-render only via their own subscription, so a field edit re-renders just the leaves that read it. Adds regression tests: stable accessor identity across a data-driven re-render (both `<Entity>` and `useEntity` paths), a memoized child holding the accessor no longer re-rendering on an unrelated data change, and `<Field>` staying live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKx7JbV7Z7EVXofqRgwop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
EntityHandleis a stateless live view over the store, butuseEntityand<Entity>recreated it on every snapshot/version change —snapshot/versionwere fed into the handle'suseMemodeps. The only reason was to handReact.memo-wrapped children a fresh reference so they'd re-render (see the oldEntityHandleRendererdoc comment: "children may be wrapped in React.memo and need a new handle reference to trigger re-renders").That reference-churn worked against the store's whole point — fine-grained reactivity:
fieldHandleCache/relationHandleCache, rebuilding them lazily. The "stable identity" promised inBaseHandle's docstring held only within one data version.<Field>/<HasOne>/<HasMany>, which already subscribe to the store themselves, were force-re-rendered redundantly.useEffect([handle])/useMemo([handle])deps in consumer code (the same identity-churn family as fix(bindx-react): stabilize useEntityList selection identity to prevent refetch loop #39's refetch loop).Change
The blessed leaf components already re-render via their own subscription (
useField/useAccessor→useSyncExternalStore), independent of the handle reference. So the recreation is redundant.snapshot/versionfrom the handleuseMemodeps inuseEntityandEntityHandleRenderer→ theEntityHandlekeeps a stable identity across data changes.accessor.valuereads inchildrenstay fresh (ergonomics preserved). Memoized leaves now re-render only through their own subscription — a field edit re-renders just the leaves that read it.The one behavioral change: a
React.memo-wrapped component that readsaccessor.valueinline without subscribing (i.e. without<Field>/useField) no longer auto-updates from a parent re-render — it must subscribe. That's the intended, teachable contract, and the primitives for it already exist and are the recommended path.Tests
tests/react/jsx/stableHandleIdentity.test.tsx:<Entity>and directuseEntitypaths;<Field>stays live (fine-grained reactivity);All three fail on the pre-change behavior (verified by temporarily restoring the deps) and pass after. Full suite: 1605 pass; the only failures are the playground browser tests (no live playground in this env — CI's
test-browserjob covers them) and one pre-existingbindx-formfailure unrelated to this change.🤖 Generated with Claude Code