Compose islands
Nest islands as children, pass props between them, and keep keyed identity across reorders.
Compose islands the way you compose function components. A child island is interpolated as a function call or a JSX element:
import { const ilha: IlhaFactoryilha } from "ilha";
const const Child: Island<{
value: string;
}>
Child = ilha<{
value: string;
}>(component: IslandComponent<{
value: string;
}>): Island<{
value: string;
}> (+3 overloads)
ilha<{ value: stringvalue: string }>(({ value: stringvalue }) => (
<"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>{value: stringvalue}</"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>
));
const const value: "v"value = "v";
<const Child: Island<{
value: string;
}>
Child value?: string | undefinedvalue={const value: "v"value} />;
const Child: Island
(props?: Partial<{
value: string;
}> | undefined) => IslandCall
Child({ value?: string | undefinedvalue });
const Child: Island<{
value: string;
}>
Child.Island<{ value: string; }>.key(key: string): KeyedIsland<{
value: string;
}>
key("stable")({ value?: string | undefinedvalue });
Nested islands
Primitive state belongs to the child island instance, not its parent. Mounting a child island inside a parent render creates an independent boundary with its own 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";
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>{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>;
});
const const App: Island<unknown>App = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => (
<"section": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLElement>>section>
<const Counter: Island<unknown>Counter />
<const Counter: Island<unknown>Counter />
</"section": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLElement>>section>
));
Each <Counter /> is a separate island instance with its own state. When the parent rerenders, mounted children keep their DOM, listeners, and state; only their props are pushed through.
Passing props
Pass props to a child the same way you would to any component. When the parent rerenders with new props, the child rerruns with those current values:
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 Badge: Island<{
label: string;
}>
Badge = ilha<{
label: string;
}>(component: IslandComponent<{
label: string;
}>): Island<{
label: string;
}> (+3 overloads)
ilha<{ label: stringlabel: string }>(({ label: stringlabel }) => (
<"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>{label: stringlabel}</"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>
));
const const App: Island<unknown>App = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const label: StateAccessor<string>label = 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("New");
return <const Badge: Island<{
label: string;
}>
Badge label?: string | undefinedlabel={const label: MarkedSignalAccessor
() => string (+1 overload)
label()} />;
});
A child’s state() initializer applies when that child instance is created. Later prop changes rerender the child but do not reset its state.
Keyed islands
In lists that reorder, insert, or remove items, use key() to give each child a stable identity. The key keeps the child instance (and its state, DOM, and focus) across reorders:
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 Item: Island<{
text: string;
}>
Item = ilha<{
text: string;
}>(component: IslandComponent<{
text: string;
}>): Island<{
text: string;
}> (+3 overloads)
ilha<{ text: stringtext: string }>(({ text: stringtext }) => {
const const open: StateAccessor<boolean>open = 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);
return (
<"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>
{text: stringtext} — {const open: MarkedSignalAccessor
() => boolean (+1 overload)
open() ? "open" : "closed"}
</"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>
);
});
const const List: Island<unknown>List = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const items: StateAccessor<string[]>items = 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(["a", "b", "c"]);
return (
<"ul": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLUListElement>>ul>
{const items: MarkedSignalAccessor
() => string[] (+1 overload)
items().Array<string>.map<JSX.Element>(callbackfn: (value: string, index: number, array: string[]) => JSX.Element, thisArg?: any): JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((text: stringtext) => (
<"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li JSX.IntrinsicAttributes.key?: ((string | number) & (string | number | RawHtml)) | undefinedkey={text: stringtext}>{const Item: Island<{
text: string;
}>
Item.Island<{ text: string; }>.key(key: string): KeyedIsland<{
text: string;
}>
key(text: stringtext)({ text?: string | undefinedtext })}</"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>
))}
</"ul": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLUListElement>>ul>
);
});
Keys must be unique within a single parent render and cannot contain the : slot separator. Ilha preserves keyed identity across reorder, insertion, and removal.
Slot wrapper tag
Each nested island is wrapped in a slot element (default div). Choose the wrapper tag with the child’s { as } constructor option — for valid structure like <li> inside <ul>:
import { const ilha: IlhaFactoryilha } from "ilha";
const const Item: Island<{
label: string;
}>
Item = ilha<{
label: string;
}>(component: IslandComponent<{
label: string;
}>, options: {
as?: string;
}): Island<{
label: string;
}> (+3 overloads)
ilha(
({ label: stringlabel }: { label: stringlabel: string }) => <"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>{label: stringlabel}</"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>,
{ as?: string | undefinedas: "li" },
);
The { as } option means different things in different places: on ilha(component, { as }) it sets the nested-island slot wrapper tag, while on hydratable(props, { as }) it sets the top-level hydration host wrapper tag. The two are independent.
JSX key also has two meanings depending on the component boundary: on an ilha() island child it becomes the slot key (identity across reorder), while on a transparent (plain) component it becomes a data-key morph key on the DOM node.
Composition and plain components
A plain function component is transparent: it belongs to the containing island, and its primitives share that island’s frame. Only ilha()-wrapped components are independent boundaries.