Skip to content

.state()

Declare reactive signals local to an island to store values that change over time and drive re-renders.

Updated View as Markdown

Declares a reactive signal local to the island. State is the primary way to store values that change over time and drive re-renders.

Adding local state is a reason to expand ilha(() => JSX) into a builder chain. The component keeps the same island boundary; the builder adds state to it.

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
state.count((previous) => previous + 1); // update from the latest value

Use the updater form when the next value depends on the previous value. Ilha calls the function once with the latest value and writes its return value.

When a signal is written, the island re-renders automatically. Only the affected island updates — nothing outside it is touched.

If the state value is itself a function, Ilha treats any function passed to the setter as an updater. Return the replacement function from an outer function:

state.callback(() => nextCallback);

Initializing from input

The initial value can be a static value or a function that receives the resolved input:

import 
const ilha: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 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: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.
IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.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<never, never>, Record<never, never>, Record<never, never>> (+2 overloads)
input
(import zz.
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 zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber().ZodType<any, any, $ZodNumberInternals<number>>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload)default(0) }))
.
IlhaBuilder<{ start: number; } & Record<string, unknown>, Record<never, never>, 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>, Record<never, never>>
state
("count", ({ start: numberstart }) => start: numberstart)
.
IlhaBuilder<{ start: number; } & Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<{
    start: number;
} & Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>, 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 }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<Record<never, never>, "count", number>>state.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>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: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
from "ilha";
const const Form: Island<RootInput, MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>>Form =
const ilha: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<string, "name">(key: "name", init?: StateInit<RootInput, string> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>state("name", "") .IlhaBuilder<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>.state<boolean, "submitted">(key: "submitted", init?: StateInit<RootInput, boolean> | undefined): IlhaBuilder<RootInput, MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, RootDerived, RootActions>state("submitted", false) .IlhaBuilder<RootInput, MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, RootDerived, RootActions>.state<number, "count">(key: "count", init?: StateInit<RootInput, number> | undefined): IlhaBuilder<RootInput, MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>, RootDerived, RootActions>state("count", 0) .IlhaBuilder<RootInput, MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>>render(({ state: IslandState<MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>>state }) => ( <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p> {state: IslandState<MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>>state.
name: MarkedSignalAccessor
() => string (+1 overload)
name
()} — {state: IslandState<MergeState<MergeState<MergeState<RootState, "name", string>, "submitted", boolean>, "count", number>>state.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}
</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p> ));

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: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
from "ilha";
const const Island: Island<RootInput, MergeState<RootState, "label", string>>Island =
const ilha: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<string, "label">(key: "label", init?: StateInit<RootInput, string> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "label", string>, RootDerived, RootActions>state("label", "<b>hello</b>") .IlhaBuilder<RootInput, MergeState<RootState, "label", string>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "label", string>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "label", string>>render(({ state: IslandState<MergeState<RootState, "label", string>>state }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<RootState, "label", string>>state.label: SignalAccessor<string>label}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);

If you call state.label() explicitly it works the same way — both forms are equivalent inside JSX.

Updating state from events

Put reusable state updates in an action, then call the action from a lowercase native event prop:

import 
const ilha: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
from "ilha";
const const Toggle: Island<RootInput, MergeState<RootState, "open", boolean>>Toggle =
const ilha: RootBuilder & DirectIslandFactory & {
    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;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<boolean, "open">(key: "open", init?: StateInit<RootInput, boolean> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "open", boolean>, RootDerived, RootActions>state("open", false) .IlhaBuilder<RootInput, MergeState<RootState, "open", boolean>, RootDerived, RootActions>.action<"toggle", undefined, void>(key: "toggle", fn: (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "open", boolean>, RootDerived>) => void): IlhaBuilder<RootInput, MergeState<RootState, "open", boolean>, RootDerived, RootActions & Record<"toggle", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "open", boolean>, RootDerived>) => void>>action("toggle", (_: undefined_, { state: IslandState<MergeState<RootState, "open", boolean>>state }) => state: IslandState<MergeState<RootState, "open", boolean>>state.
open: MarkedSignalAccessor
(value: SignalSetter<boolean>) => void (+1 overload)
open
((open: booleanopen) => !open: booleanopen),
) .IlhaBuilder<RootInput, MergeState<RootState, "open", boolean>, RootDerived, RootActions & Record<"toggle", (props: undefined, ctx: ActionContext<...>) => void>>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "open", boolean>, RootDerived, RootActions & Record<"toggle", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "open", boolean>, RootDerived>) => void>>) => string | RawHtml): Island<RootInput, MergeState<RootState, "open", boolean>>render(({ state: IslandState<MergeState<RootState, "open", boolean>>state, action: IslandActions<RootActions & Record<"toggle", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "open", boolean>, RootDerived>) => void>>action }) => ( <
"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
    popovertarget?: string;
    popoverTarget?: string;
    popovertargetaction?: "hide" | "show" | "toggle";
    popoverTargetAction?: "hide" | "show" | "toggle";
}>
button
onclick?: NativeEventHandler<PointerEvent & {
    readonly currentTarget: HTMLButtonElement;
}> | undefined
onclick
={action: IslandActions<RootActions & Record<"toggle", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "open", boolean>, RootDerived>) => void>>action.toggle: ActionAccessor<undefined, void>toggle}>
Toggle: {
var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String
(state: IslandState<MergeState<RootState, "open", boolean>>state.
open: MarkedSignalAccessor
() => boolean (+1 overload)
open
())}
</
"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
    popovertarget?: string;
    popoverTarget?: string;
    popovertargetaction?: "hide" | "show" | "toggle";
    popoverTargetAction?: "hide" | "show" | "toggle";
}>
button
>
));

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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close