Summary
src/default-theme/globals.ts creates its mobileLayout media-query watcher at module scope and applies the initial value on a setTimeout(0). This causes three user-visible problems, the worst being broken hydration (blank/errored page) on viewports ≤ 1100px for any page whose content hydrates a Dynamic-based component (e.g. anything built on @kobalte/core).
// src/default-theme/globals.ts
const [_mobileLayout, setMobileLayout] = createSignal(false);
onMount(() => { // ← module scope: no owner
const query = createMediaQuery("(max-width: 1100px)");
createRoot(() => {
createEffect(on(query, (q) => setMobileLayout(q), { defer: true }));
});
setTimeout(() => setMobileLayout(query())); // ← can fire mid-hydration
});
Symptom 1 — dev console warning on every site
onMount at module scope registers a computation without an owner:
computations created outside a `createRoot` or `render` will never be disposed
createComputation → createEffect → onMount → globals.js:4
Symptom 2 — hydration poisoned on viewports ≤ 1100px
Sequence on a full page load with a window narrower than 1100px:
- SSR renders with
mobileLayout() === false → desktop <aside> sidenav branch of the Show in Layout.tsx.
- On the client,
globals.ts is imported before hydration finishes; the leaked onMount effect runs and queues setTimeout(() => setMobileLayout(query())).
- With streaming SSR the timeout can fire while hydration is still in progress.
mobileLayout flips to true, the Show in Layout.tsx switches to the mobile Dialog branch, which no longer matches the server-rendered DOM.
- Solid throws
Hydration Mismatch. Unable to find DOM nodes for hydration key … (pointing at Layout.tsx) and sets sharedConfig.done = true.
- Every element that hydrates after that point sees
isHydrating() === false. For plain elements this happens to fall through, but Dynamic in its string case calls getNextElement() with no template argument (solid-js/web createDynamic), so it crashes:
TypeError: template is not a function
at getNextElement (solid-js/web)
at createDynamic ...
Any docs page whose MDX/content renders a Kobalte-based component (all Kobalte parts render through Polymorphic/Dynamic) dies here — the route content client-renders into an error boundary or a blank page. Pages of plain markdown mostly survive, which makes the bug easy to miss.
(As a side effect, the solid-start dev overlay that catches the error then fails itself with The requested module 'source-map-js' does not provide an export named 'SourceMapConsumer', which adds to the confusion.)
Symptom 3 — no responsive layout updates until the timeout wins the race
Cosmetic by comparison, but the initial layout state is applied at an uncontrolled time.
Reproduction
- solid-start
2.0.0-beta.0, vite 8, @kobalte/solidbase 0.6.9 (also reproduces with the current pkg.pr.new build — globals.ts is unchanged there), default theme.
- Any MDX page that renders a
@kobalte/core-based component in its content (e.g. a Kobalte Button wrapper).
- Set the browser window to < 1100px wide and hard-reload the page.
- Console shows the
Hydration Mismatch warning from Layout.tsx, then TypeError: template is not a function; the page content is blank or error-boundaried.
At > 1100px the setTimeout writes false into a signal already holding false, nothing re-renders, and everything works — the crash only affects narrower windows.
Suggested fix
Create the reactivity inside a root and defer the initial flip until hydration has settled (e.g. the window load event). This removes the leaked-computation warning and makes the mid-hydration flip impossible in practice:
import { createMediaQuery } from "@solid-primitives/media";
import { createEffect, createRoot, createSignal, on } from "solid-js";
import { isServer } from "solid-js/web";
const [_mobileLayout, setMobileLayout] = createSignal(false);
if (!isServer) {
createRoot(() => {
const query = createMediaQuery("(max-width: 1100px)");
createEffect(on(query, (q) => setMobileLayout(q), { defer: true }));
const apply = () => setTimeout(() => setMobileLayout(query()));
if (document.readyState === "complete") apply();
else window.addEventListener("load", apply, { once: true });
});
}
export const mobileLayout = _mobileLayout;
I've verified this patch locally (via bun patch): the warning disappears and pages with Kobalte-based content hydrate correctly at all widths. A deeper fix might be to render the same tree on both sides until after hydration by design, but the above stays within the current architecture.
Happy to open a PR if you'd take this shape of fix.
Summary
src/default-theme/globals.tscreates itsmobileLayoutmedia-query watcher at module scope and applies the initial value on asetTimeout(0). This causes three user-visible problems, the worst being broken hydration (blank/errored page) on viewports ≤ 1100px for any page whose content hydrates aDynamic-based component (e.g. anything built on@kobalte/core).Symptom 1 — dev console warning on every site
onMountat module scope registers a computation without an owner:Symptom 2 — hydration poisoned on viewports ≤ 1100px
Sequence on a full page load with a window narrower than 1100px:
mobileLayout() === false→ desktop<aside>sidenav branch of theShowinLayout.tsx.globals.tsis imported before hydration finishes; the leakedonMounteffect runs and queuessetTimeout(() => setMobileLayout(query())).mobileLayoutflips totrue, theShowinLayout.tsxswitches to the mobileDialogbranch, which no longer matches the server-rendered DOM.Hydration Mismatch. Unable to find DOM nodes for hydration key …(pointing atLayout.tsx) and setssharedConfig.done = true.isHydrating() === false. For plain elements this happens to fall through, butDynamicin its string case callsgetNextElement()with no template argument (solid-js/webcreateDynamic), so it crashes:Any docs page whose MDX/content renders a Kobalte-based component (all Kobalte parts render through
Polymorphic/Dynamic) dies here — the route content client-renders into an error boundary or a blank page. Pages of plain markdown mostly survive, which makes the bug easy to miss.(As a side effect, the solid-start dev overlay that catches the error then fails itself with
The requested module 'source-map-js' does not provide an export named 'SourceMapConsumer', which adds to the confusion.)Symptom 3 — no responsive layout updates until the timeout wins the race
Cosmetic by comparison, but the initial layout state is applied at an uncontrolled time.
Reproduction
2.0.0-beta.0, vite 8,@kobalte/solidbase0.6.9 (also reproduces with the currentpkg.pr.newbuild —globals.tsis unchanged there), default theme.@kobalte/core-based component in its content (e.g. a KobalteButtonwrapper).Hydration Mismatchwarning fromLayout.tsx, thenTypeError: template is not a function; the page content is blank or error-boundaried.At > 1100px the
setTimeoutwritesfalseinto a signal already holdingfalse, nothing re-renders, and everything works — the crash only affects narrower windows.Suggested fix
Create the reactivity inside a root and defer the initial flip until hydration has settled (e.g. the window
loadevent). This removes the leaked-computation warning and makes the mid-hydration flip impossible in practice:I've verified this patch locally (via
bun patch): the warning disappears and pages with Kobalte-based content hydrate correctly at all widths. A deeper fix might be to render the same tree on both sides until after hydration by design, but the above stays within the current architecture.Happy to open a PR if you'd take this shape of fix.