diff --git a/.changeset/fix-refresh-lazy-hydration.md b/.changeset/fix-refresh-lazy-hydration.md new file mode 100644 index 000000000..1c025416a --- /dev/null +++ b/.changeset/fix-refresh-lazy-hydration.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +Fix hydration crashing when a lazy component module is evaluated with Solid Refresh enabled (#2920). Refresh component registrations now use plain signals so module-level bookkeeping does not require a reactive owner or consume hydration child IDs. diff --git a/packages/solid/src/refresh/index.ts b/packages/solid/src/refresh/index.ts index 06cf32411..d278d6397 100644 --- a/packages/solid/src/refresh/index.ts +++ b/packages/solid/src/refresh/index.ts @@ -176,16 +176,15 @@ export function $$component
>( ): (props: P) => SolidElement { if (!IS_DEV) return component; let current = component; - // A writable memo: `createSignal(fn)` in 2.0 computes its value from `fn` - // on first read and accepts updater writes. `current` mirrors the latest - // component so a recompute (which has no dependencies) stays consistent. - const [comp, setComp] = createSignal<(props: P) => SolidElement>(() => current); + // Keep the component inside a plain signal so module-level registration is + // never treated as a hydration-aware computation. + const [comp, setComp] = createSignal({ current }); const update = (action: () => (props: P) => SolidElement): void => { current = action(); - setComp(() => current); + setComp({ current }); }; const instances: LiveInstances = { count: 0 }; - const proxy = createProxy(comp, id, instances, options.location); + const proxy = createProxy(() => comp().current, id, instances, options.location); registry.components.set(id, { id, component, diff --git a/packages/solid/test/client-hydration.spec.ts b/packages/solid/test/client-hydration.spec.ts index ef7f0e4b3..ae96ae386 100644 --- a/packages/solid/test/client-hydration.spec.ts +++ b/packages/solid/test/client-hydration.spec.ts @@ -22,6 +22,7 @@ import { } from "../src/client/hydration.js"; import { lazy } from "../src/client/component.js"; import { Errored, Loading } from "../src/client/flow.js"; +import { $$component, $$registry } from "../src/refresh/index.js"; // Enable the hydration-aware wrappers enableHydration(); @@ -66,6 +67,35 @@ function stopHydration() { (sharedConfig as any).loadModuleAssets = undefined; } +describe("Refresh registration during hydration", () => { + afterEach(() => { + stopHydration(); + }); + + test("registers a component without a reactive owner", () => { + startHydration({}); + + const proxy = $$component($$registry(), "LazyComponent", () => "content"); + + expect(proxy({})).toBe("content"); + }); + + test("does not consume a hydration child id", () => { + startHydration({ t0: "server" }); + + let result: unknown; + createRoot( + () => { + $$component($$registry(), "Component", () => "component"); + result = createMemo(() => "client")(); + }, + { id: "t" } + ); + + expect(result).toBe("server"); + }); +}); + describe("Error Boundary Hydration", () => { afterEach(() => { stopHydration();