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:
- Clone the repository and check out
9d295727496d194b53964fb1b8d8a86a46c895b4.
- Remove
/* @refresh reload */ from src/routes/Index.tsx.
- Run
pnpm install.
- Run
pnpm dev.
- Open
http://localhost:3000/.
- 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:
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.
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 callscreateSignalinsolid-js/dist/refresh.dev.js:90. The signal entershydratedCreateSignal, but module evaluation has no reactive owner, sopeekNextChildId()eventually reads_configfromnull.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:9d295727496d194b53964fb1b8d8a86a46c895b4./* @refresh reload */fromsrc/routes/Index.tsx.pnpm install.pnpm dev.http://localhost:3000/.The relevant route declaration is:
The development transform evaluates this at module scope in the lazy chunk:
Actual behavior
refresh.dev.js:90is the Refresh registration signal: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:
Switching
vite-plugin-solidtocompiler: '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.20vite-plugin-solid@3.0.0-next.13@solidjs/router@0.17.0-next.5vite@8.0.10nitro@3.0.260610-betaAdditional 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.