Repository navigation
Conversation
Every ElementRenderer read the whole state through StateContext and VisibilityContext, so any state write re-ran prop resolution and visibility for every element in the spec, even for paths nobody reads. Each element now collects the state paths its expressions read ($state, $bindState, $bindItem, $template, visibility conditions and watch keys) and subscribes to the store through useSyncExternalStore with a selector over those paths. Elements using a custom directive keep subscribing to the whole state model, since a directive can read anything. RepeatChildren subscribes to its statePath, and the repeat scope value is memoized so unchanged items skip re-rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
@shteynu is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
Author
|
@Railly @ctate — this builds directly on #325: that PR made spec changes cheap, this one does the same for state writes. With 1,005 elements a write to one path re-ran 1,013 element renderers before and 1 after (~20× faster per write); details and tests are in the description. Two things need someone from the team: the Vercel preview deploys are waiting on team authorization, and the GitHub Actions run needs approval since this comes from a fork. Happy to adjust the approach if you'd rather split it differently, for example landing the |
This branch has not been deployed
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.
Summary
#325 made spec changes cheap: an unchanged element no longer re-runs when a new spec arrives. State changes still re-run everything. Every
ElementRendererreads the whole state throughStateContextandVisibilityContext, and a context change bypassesReact.memo, so anysetre-resolves props and visibility for every element in the spec, including writes to paths no element reads.CatalogComponentBoundarythen stops most catalog components from rendering, but the per-element work stays O(N) per write, for example on every keystroke in a bound input.This change:
useStateSubscription()to@internal/react-state(subscribe/getSnapshot/getServerSnapshot) next to the existing per-updateStateContext$state,$bindState,$bindItem(against the repeat base path),$templatereferences, visibility conditions andwatchkeysuseSyncExternalStorewith a selector over those paths, so an element re-renders only when a value it reads changes (compared withObject.is, which relies on the store's structural sharing)resolvecan read anything fromctx.stateModelRepeatChildrento itsstatePathplus the state paths of its item filterRepeatScopeProvidervalue, so editing one item no longer re-runs the elements of every other itemPublic spec, renderer, state and binding contracts are unchanged.
useStateSubscriptionis internal and not exported from@json-render/react.Evidence
Element bodies are
ElementRendererexecutions, counted through registry lookups. Components are catalog component renders. The tree has 500 cards, each with one text, plus a repeat list, avisiblegate and a$template(1,005 spec elements). The same commit (fc2a696) was built with and without this change:visiblegate,$template, replacing a parent objectTimings are from a React development build in jsdom (production builds do not expose
act). Read them as a ratio, not as absolute numbers. On a 105-element tree the same per-write timing goes from 1.67 ms to 0.27 ms. The DOM was checked after every step in both builds.state-subscriptions.test.tsxcovers:visible,$templateand$cond, plus replacing a parent object of a read path$bindItemin a repeat, where only the edited item re-runs and appended items renderThe two isolation tests fail on
main; the other three pass on both.Checked locally:
vitest runoverpackages/react,react-state,react-pdf,react-nativeandink(the packages that use@internal/react-state): 153 passedtsc --noEmitinpackages/react: no new errors. The 23 existing errors in test files are unchanged.tsupbuilds and prettier passNot in this change
Every
ElementRendererstill consumes the action context forexecute, and that context value changes while a custom action handler runs:loadingActionsis set before the handler and cleared after it (pendingConfirmationchanges too for confirmed actions). Going byactions.tsx, each custom handler call should therefore re-run every element twice. I have not measured it. Built-in actions such assetStatereturn before that point. Splitting a stableexecuteout of the context would address it, but it is a separate change.🤖 Generated with Claude Code