#State
Declares a reactive signal local to the island. State is the primary way to store values that change over time and drive re-renders.
#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 , { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html } 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>
`,
);#Reading and writing
Each state entry becomes a signal accessor — a function that both reads and writes depending on how it is called:
state.count(); // read → returns current value
state.count(5); // write → sets value to 5When a signal is written, the island re-renders automatically. Only the affected island updates — nothing outside it is touched.
#Initializing from input
The initial value can be a static value or a function that receives the resolved input:
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 { import z z } from "zod";
const const Counter: Island<{
start: number;
} & 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>>.input<z.ZodObject<{
start: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>>(schema: z.ZodObject<{
start: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>): IlhaBuilder<{
start: number;
} & Record<string, unknown>, Record<string, never>, Record<string, never>> (+1 overload)
input (import z z .function object<{
start: z.ZodDefault<z.ZodNumber>;
}>(shape?: {
start: z.ZodDefault<z.ZodNumber>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
start: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>
object ({ start: z.ZodDefault<z.ZodNumber> start : import z z .function number(params?: string | z.core.$ZodNumberParams): z.ZodNumber number ().ZodType<any, any, $ZodNumberInternals<number>>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload) default (0) }))
.IlhaBuilder<{ start: number; } & Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<{
start: number;
} & Record<string, unknown>, number> | undefined): IlhaBuilder<{
start: number;
} & Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>
state ("count", ({ start: number start }) => start: number start )
.IlhaBuilder<{ start: number; } & Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<{
start: number;
} & Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<{
start: number;
} & 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>`);This is evaluated once at mount time. The initializer is not reactive — it only runs when the island is first created.
#Multiple state entries
Chain .state() as many times as needed. Each key becomes a typed accessor on the state object:
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 Form: Island<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>> Form = 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<string, "name">(key: "name", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "name", string>, Record<string, never>> state ("name", "")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "name", string>, Record<string, never>>.state<boolean, "submitted">(key: "submitted", init?: StateInit<Record<string, unknown>, boolean> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, Record<string, never>> state ("submitted", false)
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>, Record<string, never>> state ("count", 0)
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>, Record<...>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>> render (({ state: IslandState<MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>> state }) => `<p>${state: IslandState<MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>> state .name: MarkedSignalAccessor
() => string (+1 overload)
name ()} — ${state: IslandState<MergeState<MergeState<MergeState<Record<string, never>, "name", string>, "submitted", boolean>, "count", number>> state .count: MarkedSignalAccessor
() => number (+1 overload)
count ()}</p>`);#Inside html``
Signal accessors can be interpolated directly into html without calling them. ilha detects signal accessors and calls them automatically, and applies HTML escaping:
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";
const const Island: Island<Record<string, unknown>, MergeState<Record<string, never>, "label", string>> 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>>.state<string, "label">(key: "label", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "label", string>, Record<string, never>> state ("label", "<b>hello</b>")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "label", string>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "label", string>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "label", string>> render (({ state: IslandState<MergeState<Record<string, never>, "label", string>> state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `<p>${state: IslandState<MergeState<Record<string, never>, "label", string>> state .label: SignalAccessor<string> label }</p>`);
If you call state.label() explicitly it works the same way — both forms are equivalent inside html\``.
#Updating state from events
State accessors are plain functions, so they work directly as setters inside event handlers:
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";
const const Toggle: Island<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>> Toggle = 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<boolean, "open">(key: "open", init?: StateInit<Record<string, unknown>, boolean> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>, Record<string, never>> state ("open", false)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>, Record<string, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>, "click", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>, Record<string, never>> (+1 overload) on ("button@click", ({ state: IslandState<MergeState<Record<string, never>, "open", boolean>> state }) => state: IslandState<MergeState<Record<string, never>, "open", boolean>> state .open: MarkedSignalAccessor
(value: boolean) => void (+1 overload)
open (!state: IslandState<MergeState<Record<string, never>, "open", boolean>> state .open: MarkedSignalAccessor
() => boolean (+1 overload)
open ()))
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "open", boolean>> render (
({ state: IslandState<MergeState<Record<string, never>, "open", boolean>> state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `
<div>
${state: IslandState<MergeState<Record<string, never>, "open", boolean>> state .open: MarkedSignalAccessor
() => boolean (+1 overload)
open () ? const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `<p>Content</p>` : ""}
<button>${state: IslandState<MergeState<Record<string, never>, "open", boolean>> state .open: MarkedSignalAccessor
() => boolean (+1 overload)
open () ? "Close" : "Open"}</button>
</div>
`,
);#Sharing state across islands
State declared with .state() is local to one island. If you need to share a value across multiple islands, use context() instead, which creates a named global signal.
#Notes
- State keys must be unique within the same builder chain.
- The initial value type inferred from the second argument becomes the permanent type of the accessor. Passing a value of a different type later will cause a TypeScript error.
- State is not persisted between page loads unless you use
.hydratable()withsnapshot: trueon the server side.