Skip to content

fix: run hydration events after dynamic() spreads a string tag - #3396

Merged
ryansolid merged 5 commits into
solidjs:nextfrom
JohannesGezachew:dynamic-run-hydration-events
Sep 12, 2026
Merged

ryansolid merged 5 commits into
solidjs:nextfrom
JohannesGezachew:dynamic-run-hydration-events

Conversation

@JohannesGezachew

@JohannesGezachew JohannesGezachew commented Sep 12, 2026

Copy link
Copy Markdown

Addresses part 1 of #3386.

Compiled JSX emits runHydrationEvents() after any element carrying event handlers — packages/babel-plugin/index.js (info.topLevel && config.hydratable && results.hasHydratableEvent), which produces the _$spread(el, props, false); _$runHydrationEvents(); pair visible in the hydratable fixtures.

The string-tag branch of dynamic() binds handlers through spread() but never calls it, so events the hydration script queued for a <Dynamic component="button" onClick=…> are replayed only if some other compiled element hydrates after it and makes the call. When every interactive element is a Dynamic, queued events are dropped until _$HY.done.

This calls runHydrationEvents() after spread() while hydrating. It is cheap: the function returns immediately unless sharedConfig.events has an unqueued entry. The hydrating flag is captured before spread() so the new branch cannot see a different value from the one that selected getNextElement().

Not included

Part 2 of the issue (ambiguous SVG/HTML tags a, script, style, title taking the namespace from the tag alone) is left out: you list two options there, (a) accepting a namespace from the mount context and (b) documenting the limitation and exposing the set, and that is your call to make rather than mine.

Testing

New spec packages/web/test/hydration/dynamic-hydration-events.spec.tsx: it queues a real click on the server-rendered button the way the hydration script does, hydrates a <Dynamic component="button" onClick=…>, and asserts the handler ran. Against the previous runtime it fails with expected +0 to be 1; with this change it passes.

pnpm test for @solidjs/web on macOS 26 / Node 26, all three configs green:

config result
default 77 files, 734 tests
server 84 files, 791 passed, 2 skipped
hydration 29 files, 169 tests

prettier --check is clean on both changed files.

The createContext JSDoc told readers to reach for a module-scope
signal or store for app-wide state. With ssr: true a module is
evaluated once per process, so that state is shared by every request:
a store derived from a server function leaks one user's data into
another's render, and a module-scope signal has no owner to dispose
it. The 2.0 docs say the opposite, and the reference page is generated
from this JSDoc, so the site contradicted itself.

Also fix two smaller items in the same examples:

- The createContext and useContext @example blocks render <TodoList />,
  whose stub returns nothing, so the examples fail tsc as written.
- The handleServerFunctionRequest @example imported from
  "@solidjs/web/server-functions", which resolves to the client entry
  under the browser condition; the function is exported from
  "@solidjs/web/server-functions/server".
Compiled JSX calls runHydrationEvents() after an element that carries
event handlers, so events the hydration script queued for it are
replayed once it hydrates. The string-tag branch of dynamic() binds
handlers through spread() and never made that call, so a page whose
interactive elements are all <Dynamic> dropped queued events until
_$HY.done, unless some other compiled element happened to hydrate
afterwards and made the call for it.

Call runHydrationEvents() after spread() when hydrating. The hydrating
flag is read before spread() so the branch cannot observe a different
value than the one that chose getNextElement().
@changeset-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b018ebc

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

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

Queues a click for the server-rendered button of a <Dynamic component=
"button"> before hydrating, and asserts the handler runs. The spec fails
on the previous runtime with "expected +0 to be 1", since nothing called
runHydrationEvents() for an element whose handlers came from spread().
Defining `target` on a synthetic event left the spec throwing
"Cannot redefine property: target" once the event was dispatched again,
which failed the hydration config despite the assertion passing. Capture
a real click on the server-rendered button instead, which is also how
the hydration script collects the events it queues.
@ryansolid
ryansolid merged commit 1af28a1 into solidjs:next Sep 12, 2026
6 checks passed
@ryansolid

Copy link
Copy Markdown
Member

Merged as 1af28a1 (#3386 part 1). Capturing hydrating before the spread() was the right call. Part 2 (namespace from the mount context vs. documenting the ambiguous tags) stays open on the issue for a ruling.

Claude via Cursor

@codspeed

codspeed Bot commented Sep 12, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 54.68%

⚡ 1 improved benchmark
✅ 159 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
projection derive: write one NESTED field (reference) 809.1 µs 523.1 µs +54.68%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing JohannesGezachew:dynamic-run-hydration-events (b018ebc) with next (344ed05)

Open in CodSpeed

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