Skip to content

fix: dev overlay corrupting first navigation after hydration - #2312

Draft
brenelz wants to merge 1 commit into
solidjs:mainfrom
brenelz:fix/dev-toolbar-hydration-corruption
Draft

fix: dev overlay corrupting first navigation after hydration#2312
brenelz wants to merge 1 commit into
solidjs:mainfrom
brenelz:fix/dev-toolbar-hydration-corruption

Conversation

@brenelz

@brenelz brenelz commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

The bug

With the dev overlay enabled (the default), the first client-side navigation after any SSR page load in dev leaves the previous page's DOM on screen — both routes render at once. Easy to hit as: home → about → back → refresh → click a link. devOverlay: false makes it disappear, which is how it was tracked down.

Root cause

DevToolbar renders a <Portal> inside the hydrating tree. On the client, Portal returns a freshly created empty text node as its placeholder. In a normal client render the parent insert() puts that marker into the DOM — but during hydration DOM operations are skipped (nodes are claimed in place), so the marker never enters the document (container._$host is null) while still being recorded in insert()'s bookkeeping. The first navigation re-flattens the root children and reconciles against an array containing a phantom node that isn't in the DOM; the positional diff goes wrong and the outgoing page's nodes are orphaned instead of removed.

Two non-fixes, discovered the hard way:

  • Gating the Portal behind onMount renders it in hydration claim mode (effects flush while sharedConfig.context is still set) and steals hydration keys from streaming suspense chunks → Hydration Mismatch on the streamed content.
  • Polling sharedConfig.context doesn't help either: it's unset between stream chunks, and any reflow of the hydrated root mid-stream still breaks the pending chunks' claiming.

The fix

The toolbar now renders in its own render() root appended to document.body, so it never participates in hydration at all — mounted after DOMContentLoaded (the document stays in loading readyState for the whole SSR stream, so that's the reliable stream-end signal) plus one macrotask. Reactive wiring (error signal, server-fn store, drag handling) is unchanged since the closures cross the root boundary fine.

Also includes a second dev-overlay fix: the lazily-imported error viewer reaches @jridgewell/trace-mapping only through @solidjs/start itself, so Vite's dep scanner never discovers it and serves its CJS/UMD deps raw — every dev page load logged Uncaught (in promise) SyntaxError: ... does not provide an export named 'default' (@jridgewell/resolve-uri), and the error viewer failed to load exactly when needed. The client environment now pre-bundles it via optimizeDeps.include: ["@solidjs/start > @jridgewell/trace-mapping"] (root-level optimizeDeps is ignored with the Vite 8 environments API; it has to be per-environment).

Verification

Against a minimal reproduction (streaming SSR, shared layout, 60ms createAsync in a root provider), all previously failing cases now pass with the toolbar fully functional (buttons, panels, error viewer, drag):

case before after
warm SSR load → navigate (any delay 20ms–4s) duplicate pages ✅ clean
navigate mid-stream (~40ms) duplicate pages ✅ clean
home → about → back → refresh → quick click duplicate pages ✅ clean
console on dev page load unhandled SyntaxError rejection ✅ quiet
error viewer on a thrown error failed to load ✅ renders stack/source UI

The underlying <Portal>-under-hydration phantom-marker behavior looks like a solid-js/dom-expressions core issue worth an upstream fix; this PR removes the dev overlay's exposure to it.

🤖 Generated with Claude Code

The dev toolbar's <Portal> hands the hydrating tree a client-created
marker text node that hydration never inserts into the DOM. The phantom
entry in insert()'s bookkeeping corrupts the first reconcile after a
navigation, leaving the previous page's DOM orphaned on screen (two
pages visible at once). Render the toolbar in its own root outside the
app tree instead, mounted after DOMContentLoaded because rendering
while the SSR stream is still open steals hydration keys from pending
suspense chunks.

Also pre-bundle @jridgewell/trace-mapping for the client so the error
viewer's lazy import chain stops rejecting on raw CJS/UMD files that
the dep scanner can never discover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: afa5502

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

This PR includes changesets to release 1 package
Name Type
@solidjs/start 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

@netlify

netlify Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploy Preview for solid-start-landing-page ready!

Name Link
🔨 Latest commit afa5502
🔍 Latest deploy log https://app.netlify.com/projects/solid-start-landing-page/deploys/6a8ce33ac41978000828918e
😎 Deploy Preview https://deploy-preview-2312--solid-start-landing-page.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@solidjs/start@2312

commit: afa5502

@lxsmnsyc

Copy link
Copy Markdown
Member

seems to me that this is a start of another issue (hydration + portal interaction). I'll do some tests first before I make a conclusion.

@brenelz
brenelz marked this pull request as draft August 25, 2026 02:45
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