Create an island
Build islands with function components, typed props, and Standard Schema input validation.
An island is a function component that returns JSX or html`` directly. Callilha()` with a function to create one:
import { const ilha: IlhaFactoryilha, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state } from "ilha";
const const Counter: Island<unknown>Counter = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const count: StateAccessor<number>count = state<number>(init?: number | (() => number) | undefined): StateAccessor<number>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state(0);
return (
<"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={() => const count: MarkedSignalAccessor
(value: SignalSetter<number>) => void (+1 overload)
count((value: numbervalue) => value: numbervalue + 1)}>
{const count: MarkedSignalAccessor
() => number (+1 overload)
count()}
</"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
button>
);
});
The component reruns when a reactive value it reads during rendering changes. State, derived values, actions, effects, and error handlers are registered as primitives that persist across rerenders by call order — declare them at the top level of the component in a stable order.
Typed props
Props are ordinary current values passed straight to the component:
import { const ilha: IlhaFactoryilha } from "ilha";
const const Greeting: Island<{
name: string;
}>
Greeting = ilha<{
name: string;
}>(component: IslandComponent<{
name: string;
}>): Island<{
name: string;
}> (+3 overloads)
ilha<{ name: stringname: string }>(({ name: stringname }) => {
return <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>Hello, {name: stringname}!</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>;
});
A state initializer applies only when the instance is created. Later prop changes rerender the component but do not reset state:
import { const ilha: IlhaFactoryilha, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state } from "ilha";
const const Counter: Island<{
start: number;
}>
Counter = ilha<{
start: number;
}>(component: IslandComponent<{
start: number;
}>): Island<{
start: number;
}> (+3 overloads)
ilha<{ start: numberstart: number }>(({ start: numberstart }) => {
const const count: StateAccessor<number>count = state<number>(init?: number | (() => number) | undefined): StateAccessor<number>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state(start: numberstart);
return <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{const count: MarkedSignalAccessor
() => number (+1 overload)
count()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>;
});
Input validation with a schema
Pass a Standard Schema-compatible validator as the first argument to validate and coerce props at runtime, with defaults handled by the schema:
import { import zz } from "zod";
import { const ilha: IlhaFactoryilha } from "ilha";
const const Greeting: Island<{
name: string;
}>
Greeting = ilha<z.ZodObject<{
name: z.ZodDefault<z.ZodString>;
}, z.core.$strip>>(schema: z.ZodObject<{
name: z.ZodDefault<z.ZodString>;
}, z.core.$strip>, component: IslandComponent<{
name: string;
}>): Island<{
name: string;
}> (+3 overloads)
ilha(
import zz.function object<{
name: z.ZodDefault<z.ZodString>;
}>(shape?: {
name: z.ZodDefault<z.ZodString>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
name: z.ZodDefault<z.ZodString>;
}, z.core.$strip>
object({ name: z.ZodDefault<z.ZodString>name: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string().ZodType<any, any, $ZodStringInternals<string>>.default(def: string): z.ZodDefault<z.ZodString> (+1 overload)default("World") }),
({ name: stringname }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>Hello, {name: stringname}!</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>,
);
Validation runs during SSR, hydration, mount, and prop updates. The inferred output type flows to the component’s props.
Slot wrapper tag
When an island is nested inside another island’s render, ilha wraps it in a slot element. Choose the wrapper tag with the { as } constructor option — useful for valid structure (<li> inside <ul>), semantics, or styling:
import { const ilha: IlhaFactoryilha } from "ilha";
const const Badge: Island<unknown>Badge = ilha<unknown>(component: IslandComponent<unknown>, options: {
as?: string;
}): Island<unknown> (+3 overloads)
ilha(() => <"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>New</"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>, { as?: string | undefinedas: "span" });
Plain function components
A plain function component is transparent — it belongs to the containing island, and any primitives it calls share that island’s frame and lifecycle:
import { const ilha: IlhaFactoryilha, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state } from "ilha";
function function Label(): JSX.ElementLabel() {
const const value: StateAccessor<string>value = state<string>(init?: string | (() => string) | undefined): StateAccessor<string>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state("ready");
return <"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>{const value: MarkedSignalAccessor
() => string (+1 overload)
value()}</"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>;
}
const const App: Island<unknown>App = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => <function Label(): JSX.ElementLabel />);
Transparent components must also keep a stable primitive ordering. Promote a component to its own island boundary with ilha() when it needs independent mount, hydration, or lifecycle:
import { const ilha: IlhaFactoryilha, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state } from "ilha";
const const Label: Island<unknown>Label = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const value: StateAccessor<string>value = state<string>(init?: string | (() => string) | undefined): StateAccessor<string>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state("ready");
return <"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>{const value: MarkedSignalAccessor
() => string (+1 overload)
value()}</"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>;
});
Primitive rules
state(), derived(), action(), effect(), effect.once(), and onError() are order-based hooks:
- Call them at the component’s top level.
- Call them in the same order and with the same kind on every render.
- Call them only while an island or a plain component owned by an island is rendering.
In development, ilha detects primitive calls outside an island render, hook count changes, kind changes, and conditional registration. Put conditionals inside a primitive, not around primitive registration:
import { const ilha: IlhaFactoryilha, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state, function derived<V>(fn: (ctx: {
signal: AbortSignal;
}) => V | Promise<V> | AsyncIterable<V>): DerivedAccessor<V>
Declare derived state at this call position: synchronous computations,
Promises, or async generators. Reads track their dependencies; async work
races latest-run-wins and aborts stale runs through `ctx.signal`.derived } from "ilha";
const const App: Island<unknown>App = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const enabled: StateAccessor<boolean>enabled = state<boolean>(init?: boolean | (() => boolean) | undefined): StateAccessor<boolean>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state(false);
const const loadValue: (options: {
signal: AbortSignal;
}) => string
loadValue = (options: {
signal: AbortSignal;
}
options: { signal: AbortSignalsignal: AbortSignal }) => {
void options: {
signal: AbortSignal;
}
options;
return "loaded";
};
// invalid — conditional primitive registration
if (const enabled: MarkedSignalAccessor
() => boolean (+1 overload)
enabled()) {
const const value: StateAccessor<number>value = state<number>(init?: number | (() => number) | undefined): StateAccessor<number>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state(0);
void const value: StateAccessor<number>value;
}
// valid — the condition lives inside the derived function
const const value: DerivedAccessor<string | undefined>value = derived<string | undefined>(fn: (ctx: {
signal: AbortSignal;
}) => string | Promise<string | undefined> | AsyncIterable<string | undefined> | undefined): DerivedAccessor<string | undefined>
Declare derived state at this call position: synchronous computations,
Promises, or async generators. Reads track their dependencies; async work
races latest-run-wins and aborts stale runs through `ctx.signal`.derived(async ({ signal: AbortSignalsignal }) => {
if (!const enabled: StateAccessor<boolean>enabled) return var undefinedundefined;
return const loadValue: (options: {
signal: AbortSignal;
}) => string
loadValue({ signal: AbortSignalsignal });
});
return <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{const value: () => string | undefined (+1 overload)value() ?? "idle"}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>;
});
Authoring modes
You can author with either JSX or the `html`` tagged template. Both share the same runtime and reactivity model:
import { const ilha: IlhaFactoryilha, const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const Status: Island<unknown>Status = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<p>Ready</p>`);
Server rendering
Render an island synchronously with Island.toString(props), or await async derived values with await Island.toStringAsync(props):
import { const ilha: IlhaFactoryilha } from "ilha";
const const Counter: Island<unknown>Counter = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>Count</p>");
const const html: stringhtml = const Counter: Island<unknown>Counter.Island<unknown>.toString(props?: Partial<unknown> | undefined): stringtoString();
const const asyncHtml: stringasyncHtml = await const Counter: Island<unknown>Counter.Island<unknown>.toStringAsync(props?: Partial<unknown> | undefined): Promise<string>Async SSR: renders the island and awaits async derived values before
returning the HTML string. Always returns a Promise.toStringAsync();
Direct Island(props) calls are reserved for composing child islands inside another island render.