fix: run hydration events after dynamic() spreads a string tag - #3396
Merged
ryansolid merged 5 commits intoSep 12, 2026
Merged
Conversation
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 detectedLatest commit: b018ebc 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 |
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.
Member
Merging this PR will improve performance by 54.68%
Performance Changes
Tip Curious why performance improved? Comment Comparing |
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.
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 throughspread()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 aDynamic, queued events are dropped until_$HY.done.This calls
runHydrationEvents()afterspread()while hydrating. It is cheap: the function returns immediately unlesssharedConfig.eventshas an unqueued entry. Thehydratingflag is captured beforespread()so the new branch cannot see a different value from the one that selectedgetNextElement().Not included
Part 2 of the issue (ambiguous SVG/HTML tags
a,script,style,titletaking 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 withexpected +0 to be 1; with this change it passes.pnpm testfor@solidjs/webon macOS 26 / Node 26, all three configs green:prettier --checkis clean on both changed files.