Skip to content

fix(compiler): align ref spread hydration ids - #3228

Merged
ryansolid merged 1 commit into
solidjs:nextfrom
kavhnr:fix/ref-spread-hydration-parity
Sep 3, 2026
Merged

ryansolid merged 1 commit into
solidjs:nextfrom
kavhnr:fix/ref-spread-hydration-parity

Conversation

@kavhnr

@kavhnr kavhnr commented Sep 2, 2026

Copy link
Copy Markdown

Summary

An explicit intrinsic ref makes SSR miss the lone-spread fast path. The DOM transform handles ref separately and still passes one source directly to spread(). SSR counts the raw JSX attributes and sends the same source through mergeProps.

This change makes the Babel and native SSR transforms ignore explicit ref attributes only when they decide if one spread source can pass through. Other attributes still keep the merge path.

Reproduction

const attrs = () => ({ class: "example" });
let node;

const view = () => (
  <>
    <div ref={node} {...attrs()} />
    <button onClick={() => 1}>go</button>
  </>
);

Before this change, the relevant output is:

// DOM
_$spread(_el$, attrs, false);

// SSR
_$ssrElement("div", () => _$mergeProps(attrs), undefined, true);

mergeProps creates a memo for the function source. On the server, the div gets key 0, that memo consumes ID 1, and the button gets key 2. The client does not create the memo and expects the button at key 1. In the hydration harness, the server button stayed at before after its click because the client did not claim it with the expected key.

After this change, SSR emits:

_$ssrElement("div", attrs(), undefined, true);

The server output now has consecutive keys 0 and 1. The same nodes hydrate without warnings, the following click handler runs, and the signal update changes the text to after.

Invariant

A single effective DOM spread source passes directly to spread() and ssrElement(). An explicit intrinsic ref, including a ref directive factory, is not a spread source because the DOM transform applies it through the ref path. Ordinary attributes, events, prop:*, explicit children, and additional spreads still add DOM sources. They still use mergeProps on both sides.

The compiler is the narrow layer that knows this source count. A runtime change to mergeProps would also change valid multi-source and component behavior. Restoring a client merge for this case would undo the single-source direction established by 5230666.

PR #3105 found the memo and hydration-ID mismatch, but it proposed adding the missing merge to SSR. Commit 5230666 fixed that case in the other direction: it removed the unnecessary client merge. This change keeps that direction and covers the explicit-ref edge that its regression case did not include.

Coverage

  • Babel output for assignment refs before and after the spread, callback refs, ref directive factories, children, static objects, the static marker, hydratable and non-hydratable SSR, and client-only output.
  • Merge preservation for ordinary attributes, events, prop:*, explicit children, multiple attributes and spreads, and components.
  • Babel/native parity in all compiler modes.
  • An end-to-end hydration scenario that checks consecutive server keys, server-node reuse, no warnings, a following button click, and a signal update.

How did you test this change?

These commands passed:

  • pnpm install --frozen-lockfile
  • pnpm --filter @solidjs/babel-plugin test
  • pnpm --filter @solidjs/compiler test
  • pnpm --filter @solidjs/compiler lint
  • pnpm --filter @solidjs/web test
  • JSX_COMPILER=babel pnpm --filter @solidjs/web test
  • pnpm typecheck
  • pnpm build
  • pnpm test
  • pnpm test:integration
  • pnpm exec prettier --check packages/babel-plugin/src/ssr/element.ts packages/babel-plugin/test/ref-spread.spec.js packages/compiler/__tests__/parity-probes.test.js packages/web/test/harness/scenarios.tsx .changeset/align-ref-spread-sources.md packages/web/test/harness/__artifacts__/reactive-ref-lone-spread-id-parity.json
  • rustfmt --edition 2024 --check packages/compiler/src/ssr/transform.rs
  • git diff --check

I also ran npm run size in scripts/size. It reports the same ten limit overruns on this commit and on the exact base commit, 981718b. The values are identical: 14–215 bytes over the current limits. This compiler-only change does not change those runtime bundles.

@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 40a8770

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

This PR includes changesets to release 11 packages
Name Type
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
test-integration Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch
@solidjs/web 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

@kavhnr
kavhnr force-pushed the fix/ref-spread-hydration-parity branch from d864f5d to 0824eec Compare September 2, 2026 19:43
@ryansolid
ryansolid changed the base branch from rc6-merge to next September 3, 2026 05:16
@ryansolid
ryansolid force-pushed the fix/ref-spread-hydration-parity branch from 0824eec to 40a8770 Compare September 3, 2026 05:19
@codspeed

codspeed Bot commented Sep 3, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 136 untouched benchmarks


Comparing kavhnr:fix/ref-spread-hydration-parity (40a8770) with next (f0f7531)

Open in CodSpeed

@ryansolid
ryansolid merged commit ead7b1a into solidjs:next Sep 3, 2026
7 checks passed
ryansolid added a commit that referenced this pull request Sep 6, 2026
…fact

The parity harness rewrites every __artifacts__ file with JSON.stringify and
no trailing newline; this one landed in ead7b1a (#3228) with one, so every
web test run dirtied the tree with a one-byte diff.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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