solidjs / solidjs/solid

[2.0] lazy() hydration outside <Loading> silently halts reactivity

Open
#3,338 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

unverified bug
Dominant language
TypeScript
Stars
36.1k
Forks
1.1k
Avg merge
9h 18m
Merged PRs (30d)
195

Description

Describe the bug

In Solid 2 SSR/hydration, a code-split route rendered through lazy() without an enclosing <Loading> boundary can leave the whole reactive system halted after hydration.

The server-rendered DOM remains visible, so the page looks hydrated, but signals and event-driven updates after the lazy route no longer run. The only observable diagnostic in our app was console.error; neither window.onerror nor unhandledrejection received an event, and there was no error value that the application could handle programmatically.

This is not a request to make lazy() work without a Loading boundary. The current lazy() contract documents that it suspends through an enclosing <Loading>, and the hydration error itself says to ensure that it is inside one. The problem is the failure mode when that invariant is missed: a contained, actionable hydration error would be much safer than a page that silently stops responding.

The issue was encountered through @solidjs/router's fileRoutes() adapter, which turns code-split route entries into Solid lazy() components. The router is only the trigger; the failure is in the Solid hydration/reactivity boundary.

Your Example Website or App

No public reproduction is available yet. The minimal shape is included below; the original reproduction was in a private Solid 2 SSR app.

import { lazy } from "solid-js";
import { createRouter } from "@solidjs/router";

const Page = lazy(() => import("./Page"), undefined, "/src/Page.tsx");
const Router = createRouter({ routes: [{ path: "/", component: Page }] });

// In an SSR app, mount the router outlet directly, without <Loading>:
<Router>{props => <>{props.children}</>}</Router>;

Steps to Reproduce the Bug or Issue

  1. Run a Solid 2 SSR application with a code-split route (lazy() or fileRoutes() with code splitting enabled).
  2. Render the router's matched children directly, without an enclosing <Loading> boundary.
  3. Load the route with SSR and let the browser hydrate it.
  4. Observe that the SSR markup remains visible and the console reports the missing lazy hydration preload / Ensure it is inside a Loading boundary diagnostic.
  5. Trigger an unrelated signal update or event handler after hydration.

The route content stays on screen, but the reactive graph no longer processes subsequent updates. In our application there was no window.onerror or unhandledrejection event for the failure.

Expected behavior

If a lazy component is rendered outside the required <Loading> boundary, hydration should fail in a contained and observable way: for example, by reaching an ErrorBoundary, dispatching a browser-level error, or exposing a documented programmatic failure signal. The application should not appear hydrated while the global reactive system is silently halted.

Platform

  • OS: macOS
  • Browser: Chrome
  • solid-js: 2.0.0-rc.6
  • @solidjs/web: 2.0.0-rc.6
  • @solidjs/router: 2.0.0-next.21
  • @solidjs/vite-plugin: 3.0.0-next.39

Additional context

The application workaround is either to wrap the router outlet in <Loading fallback={null}> or to disable route code splitting. The root boundary has a separate streaming SSR trade-off: a catch-all 404 may commit as HTTP 200 after the shell flushes.

Related Solid 2 hydration/lazy fixes include #2860 and #3012, but this report is about the missing error signal and global reactive halt when the required boundary is absent.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the lazy() hydration path and the missing hydration preload diagnostic, using the minimal SSR shape with the router outlet outside a boundary. Reproduce the signal or event update after hydration and trace how the failure is handled. Done means the missing boundary produces a contained, observable error without halting unrelated reactivity.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
frontend, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.