Repository navigation
Description
Describe the bug
Since rc.19, hydrating an <Errored> that the server rendered as its fallback logs Hydration key miss when the boundary's content renders through a context provider. The same page hydrates without warnings on rc.18. We hit it with a headless dropdown menu, whose trigger renders inside a context provider.
import { createContext, createMemo, Errored, Loading } from "solid-js";
const Ctx = createContext();
function Content() {
const data = createMemo(async () => {
await Promise.resolve();
throw new Error("server read failed");
});
return (
<Ctx value={1}>
<button>
<span>{data().name}</span>
</button>
</Ctx>
);
}
export const App = () => (
<main>
<Loading fallback={<p>loading</p>}>
<Errored fallback={null}>
<Content />
</Errored>
</Loading>
</main>
);Hydration key miss for "10000100": no server-rendered element carries this key (template: <button><span>). A detached element was created instead; its subtree will not appear in the document or become interactive. …
Without the <Ctx> provider, the same tree hydrates cleanly on rc.19.
Your Example Website or App
Inline above: solid-js and @solidjs/web only.
Steps to Reproduce the Bug or Issue
- Server: render
<App />withrenderToStream, plusgenerateHydrationScript()in the head (dev build,hydratable: true). - Client:
hydrate(() => <App />, root)(dev build). - Open the page and the console.
- See the key miss for the
<button>template.
Expected behavior
The boundary keeps the server's fallback and nothing under it tries to claim server DOM, as on rc.18 and as for the same content without the provider.
Platform
- OS: Linux
- Browser: Chromium 1243 (Playwright 1.63)
- Versions:
solid-js2.0.0-rc.19,@solidjs/web2.0.0-rc.19, Node 24.21.0.hydratedCreateErrorBoundaryonnext(56a982c) is unchanged from rc.19.
Additional context
When it started. We think this comes from the fix for #4273 in rc.19 ("An <Errored> boundary hydrated in the error state the server rendered now recovers"): the client now re-runs the content of an <Errored> that the server rendered as its fallback, so it can recover.
What we think happens. hydratedCreateErrorBoundary runs the content inside hydrateWindow("", fn, undefined, ~clock), so nothing claims while fn() is on the stack. As far as we can tell, createBoundary then flattens the content in a separate computed (computed(() => flatten(read(c)))) after that window has closed. A context provider resolves its children lazily, so the provider's children are first created during that flatten, with sharedConfig.hydrating still true and no claim override in place. Their templates then look for server nodes that the fallback replaced. We may be missing something about how the window is meant to extend to later work.
Possible directions. We're not sure which fits the design best:
- Resolve the content inside the window, so lazily created children exist before it closes (what we do locally, below).
- Make "claim nothing" a property of the content's owner subtree rather than of the synchronous window, so anything created under that owner later still doesn't claim.
- Run the boundary's flatten under the same claim override as the content.
Workaround we're carrying. We patch hydratedCreateErrorBoundary in the dist builds to flatten the content once, untracked, inside the window:
() => _hydratingValue
- ? hydrateWindow("", fn, undefined, (o = getOwner(), ~clock))
+ ? hydrateWindow("", () => {
+ const content = fn();
+ try {
+ untrack(() => flatten(content));
+ } catch {}
+ return content;
+ }, undefined, (o = getOwner(), ~clock))
: fn()With it the warnings go away, the server's fallback stays when nothing changes, and the boundary still recovers when its input changes on the client. We haven't checked other lazy shapes beyond context providers, and resolving the content eagerly may have costs we haven't seen.
Activity
test(hydration): the #4348 spec stubs _$HY as the real bootstrap does
fix(hydration): a hydrated Errored's fallback keeps lazily created co…
ryansolid commentedon Oct 28, 2026
Fixed on
nextby #4357 (c7f4403); it ships in the next RC.Generated by Claude Code