Skip to content

fix(compiler): scope-wrap dynamic children holes in the ssr spread path - #540

Merged
ryansolid merged 2 commits into
ryansolid:nextfrom
brenelz:fix/ssr-spread-child-scope-wrap
Jul 9, 2026
Merged

ryansolid merged 2 commits into
ryansolid:nextfrom
brenelz:fix/ssr-spread-child-scope-wrap

Conversation

@brenelz

@brenelz brenelz commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

The hole-owner-id-scopes change added the ssr generate's scope() wrap in transformChildren (the template path) but never in createElement, the spread-element path that renders through ssrElement. The dom generate scope-wraps the matching insert accessor regardless of spread, so a shape like <a {...props}>{children()}</a> reserved a hydration id slot on the client but not on the server — every hydration key allocated after the hole drifted and following siblings went unclaimed (duplicated DOM, "unclaimed server-rendered node" warnings).

Hit in practice upgrading TanStack Router to Solid 2.0.0-beta.16, where every <Link> desynced the ids of all its following siblings.

Fix

createElement now applies the same wrap as transformChildren: dynamic children holes that can allocate hydration ids evaluate under their own owner scope.

Testing

Regression-tested end-to-end in the runtime hydration suite; the new test (packages/runtime/test/ssr/hydrate-spread-scope.spec.js) fails with the exact sibling-unclaimed symptom when the wrap is removed.

The hole-owner-id-scopes change added the ssr generate's scope() wrap in
transformChildren (the template path) but never in createElement, the
spread-element path that renders through ssrElement. The dom generate
scope-wraps the matching insert accessor regardless of spread, so a shape
like <a {...props}>{children()}</a> reserved a hydration id slot on the
client but not on the server — every hydration key allocated after the
hole drifted and following siblings went unclaimed (duplicated DOM,
'unclaimed server-rendered node' warnings). Hit in practice upgrading
TanStack Router to Solid 2.0.0-beta.16, where every <Link> desynced the
ids of all its following siblings.

createElement now applies the same wrap as transformChildren: dynamic
children holes that can allocate hydration ids evaluate under their own
owner scope. Regression-tested end-to-end in the runtime hydration suite;
the new test fails with the exact sibling-unclaimed symptom when the wrap
is removed.

Co-Authored-By: Claude Fable 5 <[email protected]>
@changeset-bot

changeset-bot Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 241ff76

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@dom-expressions/babel-plugin-jsx Patch
@dom-expressions/runtime Patch
@dom-expressions/hyperscript Patch
@dom-expressions/tagged-jsx Patch
@dom-expressions/jsx-compiler Patch

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

@brenelz

brenelz commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Might need a rust compiler change too?

…rops

mergeProps with a function source creates a memo, which consumes a
hydration child id. The ssr generate evaluated mergeProps(...) in
ssrElement's argument position — before the element's own hydration key
was allocated — while the client claims the element via getNextElement
before applying the spread. The element's id shifted by one on the
server, the client re-created it instead of claiming, and later siblings
re-synced — hiding the drift behind a single duplicated element. Hit in
practice as TanStack Router's <title ref={...} {...attrs}> duplicating on
every hydration (the ref forces the compiler into the mergeProps path).

The ssr generate now defers the merge behind a thunk when hydratable,
and ssrElement allocates the hydration key before resolving function
props, matching the client's allocation order. The runtime regression
test fails with the exact unclaimed-element symptom when either half of
the fix is reverted.

Co-Authored-By: Claude Fable 5 <[email protected]>
@ryansolid
ryansolid merged commit d3d50df into ryansolid:next Jul 9, 2026
2 checks passed
ryansolid added a commit that referenced this pull request Jul 9, 2026
…iler

Mirror #540 in the Rust ssr generate: scope-wrap dynamic children holes
of spread elements and defer hydratable mergeProps behind a thunk so
ssrElement allocates the element's hydration key before dynamic props
run. Regenerates the ssr-hydratable fixtures and parity expectations
against the updated babel output.

Co-authored-by: Cursor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants