#Hydratable
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.
#Basic usage
import const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha from "ilha";
const const MyIsland: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> MyIsland = const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha .IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> state ("count", 0).IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> render (({ state: IslandState<MergeState<Record<string, never>, "count", number>> state }) => `<p>${state: IslandState<MergeState<Record<string, never>, "count", number>> state .count: MarkedSignalAccessor
() => number (+1 overload)
count ()}</p>`);
const const html: string html = await const MyIsland: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> MyIsland
.Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string> hydratable ({ count: number count : 42 }, { HydratableOptions.name: string name : "MyIsland" });
// → '<div data-ilha="MyIsland" data-ilha-props="{"count":42}">
// <p>42</p>
// </div>'#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 |
|---|---|---|---|
name | string | — | Registry key used by mount() to find the matching island on the client |
as | string | "div" | Tag name for the wrapper element |
snapshot | boolean | object | false | Embed state and/or derived values in data-ilha-state |
skipOnMount | boolean | false | Skip 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<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha from "ilha";
import { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResult mount } from "ilha";
const const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> Counter = const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha .IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> state ("count", 0).IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> render (({ state: IslandState<MergeState<Record<string, never>, "count", number>> state }) => `<p>${state: IslandState<MergeState<Record<string, never>, "count", number>> state .count: MarkedSignalAccessor
() => number (+1 overload)
count ()}</p>`);
const const html: string html = await const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> Counter .Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string> hydratable ({}, { HydratableOptions.name: string name : "Counter" });
// client
function mount(registry: IslandRegistry, options?: MountOptions): MountResult mount ({ type Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> Counter }); // ← "Counter" matches the name aboveIf 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<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha from "ilha";
const const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> Counter = const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha .IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> state ("count", 0).IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> render (({ state: IslandState<MergeState<Record<string, never>, "count", number>> state }) => `<p>${state: IslandState<MergeState<Record<string, never>, "count", number>> state .count: MarkedSignalAccessor
() => number (+1 overload)
count ()}</p>`);
// Snapshot state only
await const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> Counter .Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string> hydratable ({ count: number count : 5 }, { HydratableOptions.name: string name : "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<string, never>, "count", number>> Counter .Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string> hydratable (
{ count: number count : 5 },
{
HydratableOptions.name: string name : "Counter",
HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot : { state?: boolean | undefined state : true, derived?: boolean | undefined derived : false },
},
);snapshot value | State snapshotted | Derived snapshotted |
|---|---|---|
false | No | No |
true | Yes | 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<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha from "ilha";
const const Island: Island<Record<string, unknown>, Record<string, never>> Island = const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.onMount(fn: (ctx: OnMountContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => (() => void) | void): IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> onMount (() => {
var console: Console console .Console.log(...data: any[]): voidThe console.log() static method outputs a message to the console.
log ("this is skipped on hydration");
})
.IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>> render (() => `<div>hello</div>`);
await const Island: Island<Record<string, unknown>, Record<string, never>> Island .Island<Record<string, unknown>, Record<string, never>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string> hydratable (
{},
{
HydratableOptions.name: string name : "my-island",
HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot : true,
HydratableOptions.skipOnMount?: boolean | undefined skipOnMount : 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 <ul> where a <div> would be invalid:
import const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha from "ilha";
const const Item: Island<Record<string, unknown>, Record<string, never>> Item = const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha .IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>> render (() => `<li>item</li>`);
await const Item: Island<Record<string, unknown>, Record<string, never>> Item .Island<Record<string, unknown>, Record<string, never>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string> hydratable ({}, { HydratableOptions.name: string name : "item", HydratableOptions.as?: string | undefined as : "li" });
// → '<li data-ilha="item">…</li>'#SSR output structure
The full rendered output looks like this:
<div data-ilha="MyIsland" data-ilha-props='{"count":42}' data-ilha-state='{"count":42}'>
<p>42</p>
</div>data-ilha— the registry key, used bymount()for discovery.data-ilha-props— serialized input props, read automatically onmount().data-ilha-state— serialized signal snapshot, only present whensnapshotis set.
#With scoped styles
If the island uses .css(), the <style> tag is included inside the wrapper regardless of the snapshot option:
<div data-ilha="Card">
<style data-ilha-css>
@scope (:scope) to ([data-ilha]) {
.title {
font-weight: 700;
}
}
</style>
<div>
<p class="title">Hello</p>
</div>
</div>#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 } from "ilha:pages";
import { registry } from "ilha:registry";
// The router calls .hydratable() internally for the matched island
const html = await pageRouter.renderHydratable(request.url, registry);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<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha , { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html } from "ilha";
import { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResult mount } from "ilha";
const const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> Counter = const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> state ("count", 0)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, "click", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> (+1 overload) on ("button@click", ({ state: IslandState<MergeState<Record<string, never>, "count", number>> state }) => state: IslandState<MergeState<Record<string, never>, "count", number>> state .count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count (state: IslandState<MergeState<Record<string, never>, "count", number>> state .count: MarkedSignalAccessor
() => number (+1 overload)
count () + 1))
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> render (
({ state: IslandState<MergeState<Record<string, never>, "count", number>> state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `
<div>
<p>Count: ${state: IslandState<MergeState<Record<string, never>, "count", number>> state .count: MarkedSignalAccessor
() => number (+1 overload)
count ()}</p>
<button>Increment</button>
</div>
`,
);
// Server — render with snapshot
const const body: string body = await const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>> Counter .Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string> hydratable (
{ count: number count : 10 },
{ HydratableOptions.name: string name : "Counter", HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot : true, HydratableOptions.skipOnMount?: boolean | undefined skipOnMount : true },
);
// Client — hydrate in place
function mount(registry: IslandRegistry, options?: MountOptions): MountResult mount ({ type Counter: Island<Record<string, unknown>, MergeState<Record<string, 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.