Renders the island wrapped in a hydration container. The output includes everything ilha.mount() needs to activate the island on the client — the rendered HTML, serialized props, and optionally a state snapshot — all embedded as data attributes on a wrapper element.
Use this method in your SSR handler whenever you want the island to become interactive in the browser without a full client-side re-render.
import const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const MyIsland: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>MyIsland = const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => <IntrinsicElements[string]: anyp>{state: IslandState<MergeState<Record<never, never>, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}IntrinsicElements[string]: anyp>);
const const html: stringhtml = await const MyIsland: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>MyIsland
.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable({ count: numbercount: 42 }, { HydratableOptions.name: stringname: "MyIsland" });
// → '
// 42
// '
Options
interface HydratableOptions {
name: string; // required
as?: string; // default: "div"
snapshot?: boolean | { state?: boolean; derived?: boolean }; // default: false
skipOnMount?: boolean; // default: false
}
Option Type Default Description namestring— Registry key used by mount() to find the matching island on the client asstring"div"Tag name for the wrapper element snapshotboolean | objectfalseEmbed state and/or derived values in data-ilha-state skipOnMountbooleanfalseSkip all .onMount() callbacks when hydrating from snapshot
The name option
The name must match the key used when registering the island in your client-side mount() or hydrate() call:
// server
import const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
import { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter = const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => <IntrinsicElements[string]: anyp>{state: IslandState<MergeState<Record<never, never>, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}IntrinsicElements[string]: anyp>);
const const html: stringhtml = await const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "Counter" });
// client
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter }); // ← "Counter" matches the name above
If the name has no match in the registry, mount() skips the element silently.
The snapshot option
Snapshots embed current signal values into data-ilha-state so the client can restore them on mount without re-computing or re-fetching.
import const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter = const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => <IntrinsicElements[string]: anyp>{state: IslandState<MergeState<Record<never, never>, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}IntrinsicElements[string]: anyp>);
// Snapshot state only
await const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable(
{ count: numbercount: 5 },
{ HydratableOptions.name: stringname: "Counter", HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: true },
);
// → data-ilha-state='{"count":5}'
// Fine-grained control
await const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable(
{ count: numbercount: 5 },
{
HydratableOptions.name: stringname: "Counter",
HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: { state?: boolean | undefinedstate: true, derived?: boolean | undefinedderived: false },
},
);
snapshot valueState snapshotted Derived snapshotted falseNo No trueYes Yes { state: true, derived: false }Yes No { state: false, derived: true }No Yes
When no snapshot is set, the island mounts fresh on the client — state initializers run again and .onMount() always fires.
The skipOnMount option
When restoring from a snapshot, you often do not want .onMount() to run — the DOM is already correct and setup work would be redundant. Set skipOnMount: true to suppress all .onMount() callbacks during hydration:
import const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Island: Island<Record<string, unknown>, Record<never, never>>Island = const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.onMount(fn: (ctx: OnMountContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => (() => void) | void): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>onMount(() => {
var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log("this is skipped on hydration");
})
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anydiv>helloIntrinsicElements[string]: anydiv>);
await const Island: Island<Record<string, unknown>, Record<never, never>>Island.Island<Record<string, unknown>, Record<never, never>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable(
{},
{
HydratableOptions.name: stringname: "my-island",
HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: true,
HydratableOptions.skipOnMount?: boolean | undefinedskipOnMount: true,
},
);
Note that skipOnMount only suppresses .onMount() — .effect() callbacks always run on mount regardless.
The as option
The wrapper element tag defaults to "div". Change it when the surrounding HTML requires a specific element — for example inside a where a would be invalid:
import const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Item: Island<Record<string, unknown>, Record<never, never>>Item = const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anyli>itemIntrinsicElements[string]: anyli>);
await const Item: Island<Record<string, unknown>, Record<never, never>>Item.Island<Record<string, unknown>, Record<never, never>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "item", HydratableOptions.as?: string | undefinedas: "li" });
// → '… '
SSR output structure
The full rendered output looks like this:
42
data-ilha — the registry key, used by mount() for discovery.
data-ilha-props — serialized input props, read automatically on mount().
data-ilha-state — serialized signal snapshot, only present when snapshot is set.
With scoped styles
If the island uses .css(), the tag is included inside the wrapper regardless of the snapshot option:
Hello
With @ilha/router
When using file-system routing, .hydratable() is called internally by renderHydratable() and renderResponse(). You typically do not call it directly — the router handles it:
import { pageRouter, registry } from "ilha:pages/server";
// The router calls .hydratable() internally for the matched island
const html = await pageRouter.renderHydratable(
request.url,
registry,
);
On the client, import from ilha:pages/client (see Router — virtual modules).
For manual setups without the router, call .hydratable() directly in your SSR handler.
Full SSR + hydration example
// server.ts
import const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha, { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter = const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, "click", Record<never, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>on("button@click", ({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) =>
state: IslandState<MergeState<Record<never, never>, "count", number>>state.count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count(state: IslandState<MergeState<Record<never, never>, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count() + 1),
)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => (
<IntrinsicElements[string]: anydiv>
<IntrinsicElements[string]: anyp>Count: {state: IslandState<MergeState<Record<never, never>, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}IntrinsicElements[string]: anyp>
<IntrinsicElements[string]: anybutton>IncrementIntrinsicElements[string]: anybutton>
IntrinsicElements[string]: anydiv>
));
// Server — render with snapshot
const const body: stringbody = await const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable(
{ count: numbercount: 10 },
{ HydratableOptions.name: stringname: "Counter", HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: true, HydratableOptions.skipOnMount?: boolean | undefinedskipOnMount: true },
);
// Client — hydrate in place
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter });
Notes
.hydratable() is always async — it awaits all .derived() values before rendering, regardless of whether the snapshot includes them.
- Props are JSON-serialized into
data-ilha-props. Values that are not JSON-serializable (functions, class instances, circular references) will cause a runtime error. Keep props plain and serializable.
- The snapshot serializes signal values at the moment
.hydratable() is called. If state changes after this point on the server, those changes are not reflected in the snapshot.