Skip to content

fix(frames): hand slot claims over from the enclosing hydration registry - #2961

Merged
ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:fix/frames-slot-claim-orphan-warning
Jul 31, 2026
Merged

fix(frames): hand slot claims over from the enclosing hydration registry#2961
ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:fix/frames-slot-claim-orphan-warning

Conversation

@brenelz

@brenelz brenelz commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Problem

Every page load of an app with client components inside server components warns, even though hydration is correct. The examples/notes dev server reports all nine of its client slot roots:

Hydration completed with 9 unclaimed server-rendered node(s):
  <form _hk="sc-84ece717-0-appView-search-0" class="search" role="search">…
  <a _hk="sc-84ece717-0-appView-editButton-0" href="/new" class="edit-button…
  <div _hk="sc-a112e1b2-0-noteListView-item#0-1" style="color:black" class="sidebar-note-list-item…
  …

The listed nodes are exactly the client slot roots — SearchField, EditButton, each SidebarNoteContent and its Show fallback, and the route outlet. They are claimed in place and fully live (the expand toggle, the search field and the route outlet all work); only the accounting is wrong.

Cause

hydrate() builds the page-wide registry with gatherHydratable, which is a blanket querySelectorAll("*[_hk]") over the container — it does not skip frame regions, so every server component's client slot roots land in the root registry.

Adoption never claims from that registry. claimRender gathers the slot range's own _hk nodes into a scoped registry, swaps it in for the synchronous render, and restores the previous one after. getNextElement deletes the key it claims from whichever registry is current, so the claim consumes the scoped copy and the root registry's copy survives to the end of hydration — where verifyHydration reports it as unclaimed server markup.

Fix

When the claim scope takes ownership of a range, drop its keys from the enclosing registry. Only the scoped pass ever claims those nodes, so nothing else loses a claim.

Test plan

  • examples/notes under pnpm dev: warning gone, sidebar expand/collapse, search and navigation still work.

🤖 Generated with Claude Code

`gatherHydratable` sweeps the whole document for `_hk` and does not skip
frame regions, so every client slot root inside a server component lands in
the page-wide registry too. Adoption claims those nodes through the scoped
registry `claimRender` gathers from the slot range, which leaves the root
registry's copies untouched — and the end-of-hydration check then reports
each correctly-claimed slot node as unclaimed server markup.

Drop the scoped registry's keys from the enclosing one when the claim scope
takes ownership. Only the scoped pass ever claims them, so nothing else
loses a claim, and the dev warning stops firing on every load of an app with
client components inside server components (the notes example warned about
all nine).

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

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 482f7fa

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

The sidebar filter IS the `?searchText` query param — the root preload
reads it, so the notes-list server component refetches whenever it
changes. The note links and the New/Edit buttons dropped it, so clicking
a note mid-search cleared the box and morphed the list back to the
unfiltered set. Route navigation state through a small useSearchLink()
helper that appends the current filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
// up in the root registry too. Only this scoped registry ever claims them,
// so hand ownership over — otherwise the root's completion check reports
// each claimed slot node as unclaimed server markup.
if (prevRegistry) for (const key of registry.keys()) prevRegistry.delete(key);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this is a bad fix

@ryansolid
ryansolid merged commit c94fc5f into solidjs:next Jul 31, 2026
1 of 4 checks passed
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