Skip to content
Ilha
Esc
navigateopen⌘Jpreview
On this page

SSR and hydration

How ilha renders HTML on the server and restores interactivity in the browser through hydration.

Server-side rendering (SSR) turns an island into HTML on the server. Hydration restores that HTML to a live, interactive island in the browser. This page is the mental model; each method page below carries the details.

Choose a render mode

You pick a server API by whether you need async work (derived values, streams) and whether you want hydration markup.

API Env Output
Island.toString(props) Server Synchronous HTML
await Island.toStringAsync(props) Server Async HTML, by name
await Island(props) Server Async HTML shorthand
await Island.hydratable(props, options) Server HTML plus hydration data
mount({ Island }) Client Discovers and mounts hosts

Use Island.toString(props) for plain markup that has no async work. Async SSR awaits derived values and streamed state, so prefer await Island.toStringAsync(props) — or its shorthand await Island(props) — whenever the island may resolve async data. You never call Island(props) unawaited; the caller always awaits it.

Why toStringAsync() exists

Island.toString() always renders synchronously — derived values that are still loading render in their loading state, and streamed keys fall back to their initial value. Island.toStringAsync() returns a Promise and awaits async derived values and the first streamed value before producing HTML. Prefer it by name when you want explicit async SSR:

import { 
const ilha: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 6 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
} from "ilha";
const const Island: Island<RootInput, RootState>Island =
const ilha: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 6 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.derived<"user", any>(key: "user", fn: DerivedFn<RootInput, RootState, any>): IlhaBuilder<RootInput, RootState, RootDerived & Record<"user", any>, RootActions>derived("user", async () => { const const res: Responseres = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
fetch
("/api/user");
return const res: Responseres.Body.json(): Promise<any>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)
json
();
}) .IlhaBuilder<RootInput, RootState, RootDerived & Record<"user", any>, RootActions>.render(fn: (ctx: RenderContext<RootInput, RootState, RootDerived & Record<"user", any>, RootActions>) => string | RawHtml): Island<RootInput, RootState>render(({ derived: IslandDerived<RootDerived & Record<"user", any>>derived }) => { if (derived: IslandDerived<RootDerived & Record<"user", any>>derived.user: DerivedAccessor<any>user.loading: booleanloading) return <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>Loading…</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>; return <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{derived: IslandDerived<RootDerived & Record<"user", any>>derived.user: () => any (+1 overload)user()?.name}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>; }); // Async — waits for derived values const const html: stringhtml = await const Island: Island<RootInput, RootState>Island.Island<RootInput, RootState>.toStringAsync(props?: Partial<RootInput> | undefined): Promise<string>
Async SSR: renders the island and awaits async `.derived()` values before returning the HTML string. Always returns a Promise — prefer this over `await island(props)` when you need the async SSR form by name.
toStringAsync
();

Hydration markup

await Island.hydratable(props, options) emits HTML wrapped so the client can restore serialized props and snapshots when it calls mount({ Island }):

import { 
const ilha: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 6 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
} from "ilha";
const const Island: Island<RootInput, MergeState<RootState, "count", number>>Island =
const ilha: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 6 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<number, "count">(key: "count", init?: StateInit<RootInput, number> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>state("count", 0) .IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "count", number>>render(({ state: IslandState<MergeState<RootState, "count", number>>state }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<RootState, "count", number>>state.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);
// On the server: await const Island: Island<RootInput, MergeState<RootState, "count", number>>Island.Island<RootInput, MergeState<RootState, "count", number>>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "my-island" });

On the client, mount({ Island }) auto-discovers [data-ilha="IslandName"] elements and hydrates them.

What runs where

Concern Runs on server Runs on client
Render function Yes Yes, on re-render
.derived() sync Yes Yes
.derived() async Yes (awaited in async SSR) Yes
.stream() Pulls first value only Consumes continuously
.action() No-op (idle) Yes
.on() / .effect() No Yes
.onMount() No — client-only Yes
.onError() / onUncaughtError() No Yes

.onMount() is client-only: SSR never invokes it, matching .on() and .effect(). Server-rendered markup must not depend on onMount side effects. If you previously seeded server-visible state from input inside onMount, migrate that work to a .state() initializer, module scope, or .derived().

Linking the method pages

Was this page helpful?