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

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 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<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() with snapshot: true on the server side.