Skip to content

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

Open
kavhnr wants to merge 1 commit into
solidjs:rc6-mergefrom
kavhnr:fix/ref-spread-hydration-parity
Open

fix(compiler): align ref spread hydration ids#3228
kavhnr wants to merge 1 commit into
solidjs:rc6-mergefrom
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: 0824eec

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
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.

1 participant