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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions