Skip to content
Ilha
Esc
navigateopen⌘Jpreview
On this page

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:

  1. Call them at the component’s top level.
  2. Call them in the same order and with the same kind on every render.
  3. 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.

Was this page helpful?