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.

Interactive Tutorial

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.