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
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 5
When 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<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 { import zz } from "zod";
const const Counter: Island<{
start: number;
} & 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>>.input<z.ZodObject<{
start: z.ZodDefault<z.ZodNumber>;
}, "strip", z.ZodTypeAny, {
start: number;
}, {
start?: number | undefined;
}>>(schema: z.ZodObject<{
start: z.ZodDefault<z.ZodNumber>;
}, "strip", z.ZodTypeAny, {
start: number;
}, {
start?: number | undefined;
}>): IlhaBuilder<{
start: number;
} & Record<string, unknown>, Record<never, never>, Record<never, never>> (+1 overload)
input(import zz.object<{
start: z.ZodDefault<z.ZodNumber>;
}>(shape: {
start: z.ZodDefault<z.ZodNumber>;
}, params?: z.RawCreateParams): z.ZodObject<{
start: z.ZodDefault<z.ZodNumber>;
}, "strip", z.ZodTypeAny, {
start: number;
}, {
start?: number | undefined;
}>
export object
object({ start: z.ZodDefault<z.ZodNumber>start: import zz.function number(params?: z.RawCreateParams & {
coerce?: boolean;
}): z.ZodNumber
export number
number().ZodType<number, ZodNumberDef, number>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload)default(0) }))
.IlhaBuilder<{ start: number; } & Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<{
start: number;
} & Record<string, unknown>, number> | undefined): IlhaBuilder<{
start: number;
} & Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>
state("count", ({ start: numberstart }) => start: numberstart)
.IlhaBuilder<{ start: number; } & Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<{
start: number;
} & Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<{
start: number;
} & 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>);
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<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 Form: Island<Record<string, unknown>, MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>>Form = 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<string, "name">(key: "name", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "name", string>, Record<never, never>>state("name", "")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "name", string>, Record<never, never>>.state<boolean, "submitted">(key: "submitted", init?: StateInit<Record<string, unknown>, boolean> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, Record<never, never>>state("submitted", false)
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>, Record<never, never>>state("count", 0)
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>, Record<...>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>>render(({ state: IslandState<MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>>state }) => (
<IntrinsicElements[string]: anyp>
{state: IslandState<MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>>state.name: MarkedSignalAccessor
() => string (+1 overload)
name()} — {state: IslandState<MergeState<MergeState<MergeState<Record<never, never>, "name", string>, "submitted", boolean>, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}
</IntrinsicElements[string]: anyp>
));
Inside JSX
Signal accessors can be rendered directly in JSX without calling them. ilha detects signal accessors and calls them automatically, and applies HTML escaping:
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>, MergeState<Record<never, never>, "label", string>>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>>.state<string, "label">(key: "label", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "label", string>, Record<never, never>>state("label", "<b>hello</b>")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "label", string>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "label", string>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "label", string>>render(({ state: IslandState<MergeState<Record<never, never>, "label", string>>state }) => <IntrinsicElements[string]: anyp>{state: IslandState<MergeState<Record<never, never>, "label", string>>state.label: SignalAccessor<string>label}</IntrinsicElements[string]: anyp>);
If you call state.label() explicitly it works the same way — both forms are equivalent inside JSX.
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<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 Toggle: Island<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>>Toggle = 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<boolean, "open">(key: "open", init?: StateInit<Record<string, unknown>, boolean> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>, Record<never, never>>state("open", false)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>, Record<never, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>, "click", Record<never, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>, Record<never, never>>on("button@click", ({ state: IslandState<MergeState<Record<never, never>, "open", boolean>>state }) => state: IslandState<MergeState<Record<never, never>, "open", boolean>>state.open: MarkedSignalAccessor
(value: boolean) => void (+1 overload)
open(!state: IslandState<MergeState<Record<never, never>, "open", boolean>>state.open: MarkedSignalAccessor
() => boolean (+1 overload)
open()))
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "open", boolean>>render(({ state: IslandState<MergeState<Record<never, never>, "open", boolean>>state }) => (
<IntrinsicElements[string]: anydiv>
{state: IslandState<MergeState<Record<never, never>, "open", boolean>>state.open: MarkedSignalAccessor
() => boolean (+1 overload)
open() ? <IntrinsicElements[string]: anyp>Content</IntrinsicElements[string]: anyp> : ""}
<IntrinsicElements[string]: anybutton>{state: IslandState<MergeState<Record<never, never>, "open", boolean>>state.open: MarkedSignalAccessor
() => boolean (+1 overload)
open() ? "Close" : "Open"}</IntrinsicElements[string]: anybutton>
</IntrinsicElements[string]: anydiv>
));
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() with snapshot: true on the server side.