Skip to content

Core Concepts

Learn the core ideas behind ilha — islands, signals, JSX rendering, and the builder chain.

Updated View as Markdown

ilha is built around a small set of ideas: islands, signals, JSX rendering, and a builder-based API. Once these click, the rest of the library feels straightforward.

Islands

An island is a self-contained UI component that can render itself to HTML on the server and mount itself in the browser. It owns its own state, behavior, and rendering, so each piece of interactivity stays local and explicit.

This makes ilha a good fit for server-rendered pages that only need interactivity in specific places. Instead of turning the whole page into one client app, you can activate only the parts that need to be interactive.

A plain JSX function is not an island boundary. It is a transparent component whose events and reactive reads belong to the containing island. Wrap it with ilha() only when it needs independent ownership.

Choose the smallest component form

Start with the least machinery your component needs:

Form Ownership
const View = () => JSX The containing island owns its rendering, events, and cleanup
const View = ilha(() => JSX) View owns an independent reactive scope, lifecycle, mount, and hydration
ilha.state(...).render(...) The island also owns builder capabilities such as state, derived values, and actions

Promotion keeps the markup intact. A component without props can wrap the same render function directly. For a component with props, read them from input or keep the plain view as a child:

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 LabelView: ({ label }: {
    label: string;
}) => JSX.Element
LabelView
= ({ label: stringlabel }: { label: stringlabel: string }) => (
<"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>{label: stringlabel}</"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span> ); const
const Label: Island<{
    label: string;
}, RootState>
Label
=
ilha<{
    label: string;
}>(fn: (ctx: RenderContext<{
    label: string;
}, RootState, RootDerived, RootActions>) => string | RawHtml): Island<{
    label: string;
}, RootState>
ilha
<{ label: stringlabel: string }>(({
input: {
    label: string;
}
input
}) => (
<
const LabelView: ({ label }: {
    label: string;
}) => JSX.Element
LabelView
label: stringlabel={
input: {
    label: string;
}
input
.label: stringlabel} />
));

Replace the shorthand with a builder chain when the island needs another capability.

Isomorphic components

The same island can be used in two ways:

  • Rendered to an HTML string for SSR.
  • Mounted into a DOM element for client-side interactivity.

That means you do not have to split a component into separate “server” and “client” versions. One definition can handle both output and activation.

Signals

ilha uses signals for reactive state. A signal is a value you can read and update, and when it changes, the island reacts to that change.

A state accessor works as both a getter and setter:

const count: MarkedSignalAccessor
() => number (+1 overload)
count
(); // read
const count: MarkedSignalAccessor
(value: SignalSetter<number>) => void (+1 overload)
count
(5); // write

Inside JSX, you can render the signal value directly:

<p>{state.count()}</p>

This keeps reactive state small and direct. You read what you need, update what you need, and the island updates accordingly.

Configure an island

Use the fluent builder chain when an island needs local capabilities. Each method adds one capability, and .render() finalizes the component.

A typical island might include:

This step-by-step structure is one of the core design ideas in ilha. Instead of putting everything in one large options object, you compose behavior in a readable chain. An island with no builder capabilities can use the shorter ilha(() => JSX) form.

JSX rendering

ilha can render islands with JSX. Configure TypeScript with jsxImportSource: "ilha", then return JSX from .render().

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 Message: Island<RootInput, RootState>Message = ilha<RootInput>(fn: (ctx: RenderContext<RootInput, RootState, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, RootState>ilha(() => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{const userInput: "Ilha is awesome"userInput}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);

JSX output follows safe rendering rules: interpolated values are escaped by default, arrays render without commas, and ilha values such as child islands can be nested directly. If you really need to inject trusted markup, you can opt into that explicitly with raw().

Derived values

Not every value belongs in local state. Sometimes a component needs data that depends on state or input, including async data.

That is what .derived() is for. Each derived entry is a signal accessor — read it with derived.name(), the same way you read state.count(). You can also write derived.name(value) for optimistic UI.

For async work, the accessor also exposes loading, value, and error, so loading and error states stay part of the normal rendering model instead of something bolted on from the outside.

Events and effects

ilha separates user interaction from side effects.

Use native handlers and .action() for DOM events and reusable operations. Use .effect() when you want reactive behavior that runs after mount and reruns when its dependencies change. Use .onMount() when something should run once after the island is attached to the DOM.

This separation helps keep component logic easier to scan:

  • Events respond to user actions.
  • Effects respond to reactive changes.
  • Mount hooks handle lifecycle setup.

Scoped styles

ilha supports component-level styles with .css(). Styles are scoped to the island so they stay local and do not leak into nested child islands.

This lets you keep structure, behavior, and styling close together when that is useful, without giving up isolation.

SSR and hydration

ilha is designed to work naturally with server rendering and hydration. You can render HTML on the server, send it to the browser, and later activate the island in place.

When using hydratable output, ilha can also embed snapshots of state and derived values. That helps restore the component without unnecessary work on first mount.

Mental model

A useful way to think about an island is:

  • Input is data coming in.
  • State is reactive data owned by the component.
  • Derived is data computed from input or state.
  • Render turns all of that into HTML.
  • Mount activates behavior in the browser.

Keep this model in mind as you use the API. Each builder method adds one piece to the flow.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close