Skip to content

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

Description

@ethan-huo

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions