Skip to content

Lazy component hydration crashes in Solid Refresh module registration #2920

Description

@brenelz

Describe the bug

Hydrating a server-rendered lazy() component crashes in development when Solid Refresh wraps the component exported by the dynamically imported module.

The lazy module is evaluated while hydration is active. Its generated top-level $$component(...) registration calls createSignal in solid-js/dist/refresh.dev.js:90. The signal enters hydratedCreateSignal, but module evaluation has no reactive owner, so peekNextChildId() eventually reads _config from null.

Solid catches the failed hydration module preload and falls back to client rendering.

Reproduction

https://github.com/brenelz/solid-2/tree/9d295727496d194b53964fb1b8d8a86a46c895b4

The reproduction currently includes the /* @refresh reload */ workaround. To reproduce:

  1. Clone the repository and check out 9d295727496d194b53964fb1b8d8a86a46c895b4.
  2. Remove /* @refresh reload */ from src/routes/Index.tsx.
  3. Run pnpm install.
  4. Run pnpm dev.
  5. Open http://localhost:3000/.
  6. Observe the hydration failure in the browser console.

The relevant route declaration is:

const Index = lazy(() => import('./routes/Index.tsx'))

<Route path="/" component={Index} />

The development transform evaluates this at module scope in the lazy chunk:

const Index = _$$component(_REGISTRY, "Index", function Index() {
  // ...
})

Actual behavior

Hydration module preload failed, falling back to client render: TypeError: Cannot read properties of null (reading '_config')
    at childId (dev.js:1354:18)
    at peekNextChildId (dev.js:1386:10)
    at hydrateSignalFromAsyncIterable (dev.js:289:22)
    at hydratedCreateSignal (dev.js:422:20)
    at createSignal (dev.js:612:53)
    at $$component (refresh.dev.js:90:27)
    at Index.tsx:4:16

refresh.dev.js:90 is the Refresh registration signal:

const [comp, setComp] = createSignal(() => current);

Expected behavior

A server-rendered lazy component should hydrate in development with Solid Refresh enabled. Refresh's module-level bookkeeping signals should not consume hydration IDs or require a hydration owner.

Workarounds

Either workaround prevents the crash:

/* @refresh reload */

on every lazy route module, or globally:

solid({
  ssr: true,
  refresh: { disabled: true },
})

Switching vite-plugin-solid to compiler: 'babel' does not help because the Refresh transform still emits the same $$component(...) wrapper.

Versions

  • solid-js@2.0.0-beta.20
  • @solidjs/web@2.0.0-beta.20
  • vite-plugin-solid@3.0.0-next.13
  • @solidjs/router@0.17.0-next.5
  • vite@8.0.10
  • nitro@3.0.260610-beta
  • macOS, Chrome

Additional context

This is related to hydration plus Solid Refresh, but distinct from #2919, which reports duplicate streamed boundary content after an HMR replacement. This issue occurs on the initial hydration as soon as a Refresh-wrapped lazy module is imported.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions