Configuring SolidStart Island Hydration Directives
Enabling islands mode in SolidStart is a two-line change; getting a mostly static page to actually ship only its islands is the real work. The difference between the two is where your reactive state lives and how you mark browser-only widgets. This guide is the configuration companion to SolidStart Islands & Partial Hydration: it walks through the app.config.ts flags, structuring island entry points, using clientOnly, and then verifying that the hydration split is real rather than aspirational.
Prerequisites
Where Hydration Happens Before and After Islands Mode
The configuration only pays off if you understand what it changes. The diagram contrasts a default full-route hydration with the islands-mode split you are configuring.
Implementation Steps
Step 1 — Enable islands mode in app.config.ts
Goal: Switch SolidStart from full-route hydration to island-scoped hydration.
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
// SSR must stay on — islands mode narrows hydration, it does not replace SSR.
ssr: true,
// The experimental flag that turns on island-scoped hydration. With this set,
// route markup is static unless a component is used as an interactive island.
experimental: { islands: true },
});
Expected output: The dev server restarts cleanly. Nothing visibly changes yet, because you have not separated static from interactive components — that is Steps 2 and 3. If the server fails to boot, the flag name or config shape is wrong for your @solidjs/start version; re-check against the version you have installed.
Step 2 — Mark island entry points by isolating state
Goal: Ensure only genuinely interactive components hydrate, by keeping all reactivity inside dedicated island leaves.
The route and layout must be presentational. Any signal, effect, or store at the route level forces SolidStart to hydrate the route to keep that state alive.
// ~/routes/post/[id].tsx
// PRESENTATIONAL route — no signals, no effects. It stays static markup.
import LikeButton from "~/components/LikeButton";
import CommentBox from "~/components/CommentBox";
export default function PostRoute(props: { post: { id: string; body: string } }) {
return (
<article>
<h1>{props.post.title}</h1>
{/* Static body — never hydrates */}
<div innerHTML={props.post.body} />
{/* Each island is a small interactive leaf that ships its own chunk. */}
<LikeButton postId={props.post.id} initial={props.post.likes} />
<CommentBox postId={props.post.id} />
</article>
);
}
// ~/components/LikeButton.tsx
// The island itself. All reactivity is contained here, so ONLY this component
// hydrates — the surrounding article stays inert.
import { createSignal } from "solid-js";
export default function LikeButton(props: { postId: string; initial: number }) {
const [likes, setLikes] = createSignal(props.initial);
return (
<button onClick={() => setLikes((n) => n + 1)}>♥ {likes()}</button>
);
}
Expected output: After a rebuild, the network payload for the route requests the LikeButton and CommentBox chunks only. The article markup carries no hydration code.
Step 3 — Add clientOnly for browser-only widgets
Goal: Mount components that cannot server-render purely on the client, without hydration mismatches.
// ~/routes/analytics.tsx
import { clientOnly } from "@solidjs/start";
// Chart reads canvas/window at construction, so it must not run on the server.
// clientOnly renders `fallback` during SSR and mounts the real component only
// in the browser — no server HTML to mismatch against.
const UsageChart = clientOnly(() => import("~/components/UsageChart"));
export default function Analytics() {
return (
<main>
<h1>Analytics</h1>
{/* Sized fallback reserves layout height so the client mount causes no shift. */}
<UsageChart fallback={<div style="min-height:360px">Loading chart…</div>} />
</main>
);
}
Expected output: View source shows the fallback (Loading chart…) in the server HTML, not the chart. The chart appears after the client mounts, with no console hydration warning.
Step 4 — Confirm serialisable props
Goal: Guarantee island props survive the server-client handoff.
Islands receive their initial state from the server as serialised data. Pass only plain values; reconstruct richer types inside the island.
// WRONG — a Date instance and a callback cannot cross the boundary cleanly.
<LikeButton created={new Date()} onLike={() => track()} initial={0} />
// RIGHT — pass an ISO string and handle side effects inside the island.
<LikeButton createdISO={created.toISOString()} initial={0} />
Expected output: No serialisation warnings in the server log, and the island receives its props intact on hydration. See cross-boundary prop passing for the general pattern.
Verification
-
Network payload. In DevTools → Network, filter for JS and reload a content-heavy route. You should see only your island chunks — no route-wide bundle. If a large shared chunk loads on a static route, state is leaking up to the route level (see Troubleshooting).
-
Mount attribution. Temporarily add
onMount(() => performance.mark(\hydrated:${props.postId}`))inside each island. Read the marks with aPerformanceObserver`. Every island should log exactly once; no static component should log at all. -
View-source check for
clientOnly. Confirm the server HTML contains the fallback, not the widget, for everyclientOnlycomponent. If the widget markup is present in view-source, it is being server-rendered andclientOnlyis not applied. -
No hydration warnings. Load each route with the console open. A clean console — no mismatch or reconciliation warnings — confirms server and client output agree for every island.
Troubleshooting
The entire route hydrates despite islands mode being enabled
Root cause: Reactive state lives at the route or layout level. A createSignal, createEffect, createResource, or context provider in a route/layout component forces SolidStart to hydrate that component — and everything it wraps — to keep the state live.
Fix: Move all reactivity into island leaf components. Routes and layouts must be presentational: they may read props and render markup, but must not own signals, effects, or resources that need to stay reactive after load.
// WRONG — signal in the route hydrates the whole thing.
export default function Page() {
const [tab, setTab] = createSignal("a"); // ← forces route hydration
return <Layout>…</Layout>;
}
// RIGHT — the stateful tabs are their own island.
export default function Page() {
return <Layout><TabsIsland /></Layout>;
}
Hydration mismatch warning on an island
Root cause: The island renders non-deterministic output — Date.now(), Math.random(), or locale-dependent formatting — so the server HTML and the client’s first render differ. Solid discards the server DOM for that subtree and re-creates it, sometimes with a visible flash.
Fix: Make the island’s initial render deterministic. Compute volatile values on the server and pass them as props, or move a genuinely browser-dependent widget to clientOnly so there is no server output to diverge from.
`clientOnly` component still renders on the server (visible in view-source)
Root cause: The component is imported and rendered directly rather than through the clientOnly wrapper, so SSR still executes it.
Fix: Import the component only via clientOnly(() => import("...")) and render the returned component, never the module’s default export directly. Provide a fallback so SSR has something sized to render in its place.
// WRONG — direct import server-renders the widget.
import UsageChart from "~/components/UsageChart";
// RIGHT — clientOnly wrapper defers it to the browser.
const UsageChart = clientOnly(() => import("~/components/UsageChart"));
Island receives undefined props in the browser
Root cause: A prop value is not serialisable — a function, Date, Map, Set, or class instance — so it is dropped or corrupted at the server-client handoff.
Fix: Pass plain JSON-serialisable data (numbers, strings, plain objects/arrays) and reconstruct richer types inside the island. Handle callbacks with the island’s own event handlers rather than passing functions in as props. See cross-boundary prop passing.
Related
- SolidStart Islands & Partial Hydration — the parent guide on why fine-grained reactivity plus islands mode drives hydration cost down.
- Astro Islands and Client Directives — a per-directive islands model for comparison with SolidStart’s approach.
- Cross-Boundary Prop Passing — serialising island props so they survive the server-client handoff.
← Back to SolidStart Islands & Partial Hydration