feat(universal): spread() with fewer reactive nodes and a sources array; compilers emit it - #3424
Merged
Merged
Conversation
…ay; compilers emit it Port of @solidjs/web's spread (#3388, #3419) to the universal renderer: - `ref` folds into the props effect and is re-applied only when its identity changes. `ref()` runs the callback untracked with no owner, so nothing a ref creates is disposed when the effect reruns. - Children keep their own owned `insert` — that effect owns the child subtree, and merging it would rebuild the children on every prop change. A plain object whose `children` is a data property inserts the value with no effect at all. Three nodes become two with children through the spread, one without. - A lone function source resolves inside each tracking scope; an ARRAY of sources is the union of their keys, later sources winning, only the winning source read, function sources called inline with no merge and no memo. Nullish sources are empty. Keys are still enumerated with for...in so a renderer's proxy props answer through their traps. Both compilers' universal generate follows the dom generate: a lone spread passes straight through (reactive included — the `mergeProps(() => expr)` wrap that existed only because the runtime could not resolve a function is gone), and several sources compile to the array instead of a mergeProps() call. Universal has no hydration ids, so the runtime and compiler halves land together. Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 461e936 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Coverage Report for CI Build 34815768521Coverage remained the same at 71.842%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
7 tasks
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.
Universal counterpart of #3419 (runtime) and #3423 (compilers), in one PR: universal has no hydration ids, so the runtime and compiler halves don't need to be sequenced.
Runtime —
@solidjs/universalspread()Follows the
@solidjs/webcontract (#3388):reffolds into the props effect, applied in the commit half only when its identity changed. The renderer'sref()runs the callback untracked with a null owner, so nothing a ref creates is disposed by the fold.refis never passed tosetProperty.insert— that effect owns the child subtree, and folding it would rebuild the children on every prop change. A plain object whosechildrenis a data property inserts the value directly with no effect; a getter keeps the tracking scope. Three nodes → two with children through the spread, one without.mergeProps(() => …)).spread(node, [a, b], skipChildren): union of keys, later sources win, only the winning source is read (a shadowed getter never runs), function sources called inline with no merge and no memo, nullish sources skipped. Keys are still enumerated withfor…inso a renderer's proxy props answer through their traps (the existing reactive-update spec relies on that).RendererEffectOptionsname is still shared by the child insertion and the props effect (Expose diagnostic names for renderer-owned effects in @solidjs/universal #3063); therenderer spread reffallback name goes away with the effect it named.Compilers — universal generate (Babel + Oxc)
Same rule as the dom generate after #3423: a lone spread passes straight through — reactive included, so
spread(el, () => getProps("test"), …)instead ofspread(el, mergeProps(() => getProps("test")), …)— and several sources compile to an array literal instead of amergeProps()call. Component spreads are untouched.Tests
packages/universal/test/spread-sources.spec.js(11 cases): ref applied once and only on identity change, ref survives prop reruns without disposing what it created, ref arrays, children not rebuilt on prop change, data-property children with no effect,skipChildren, later-wins with shadowed getter unread, function source called inline / tracked / nullish siblings skipped / dropped keys removed, children from the last source, ref through sources, lone accessor, nullish source. Universal suite 3 files / 54 tests.__universal_fixtures__and Oxcuniversal+dynamic-universal). Babel 29/257, Oxccargo test+ 39 files / 5791; cross-mode parity unchanged.Rebases cleanly on #3423 (disjoint files); either order.