Signals

Reactive signals are the primitive that powers state in ilha. In addition to .state() (local to an island), ilha exports four signal helpers for cross-island sharing, performance, and control:

HelperPurpose
signal()Create a free-standing signal for one-off shared state
context()Create a named global signal accessible from anywhere by key
batch()Group multiple writes into a single propagation pass
untrack()Read a signal without subscribing the surrounding scope

signal(initial)

Creates a free-standing reactive signal that lives outside any island. Useful for sharing state across multiple islands without prop drilling, or for binding form inputs to module-level state.

Basic usage

import { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
} from "ilha";
const const count: SignalAccessor<number>count = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
const count: MarkedSignalAccessor
() => number (+1 overload)
count(); // → 0 (read)
const count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count
(5); // → sets to 5 (write)

Reading the signal inside any reactive scope — .render(), .derived(), .effect() — automatically subscribes that scope, so when the signal changes, dependents re-run as if it were local state.

Sharing state between islands

Because signal() returns a plain accessor, you can import it into any island. When one island writes to it, all others that read it re-render automatically:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha, { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
} from "ilha"; const const cartCount: SignalAccessor<number>cartCount = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
const const CartButton: Island<Record<string, unknown>, Record<never, never>>CartButton =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, Record<never, never>, "click", Record<never, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>on("button@click", () =>
const cartCount: MarkedSignalAccessor
(value: number) => void (+1 overload)
cartCount
(
const cartCount: MarkedSignalAccessor
() => number (+1 overload)
cartCount
() + 1))
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anybutton>Add to cart</IntrinsicElements[string]: anybutton>); const const CartBadge: Island<Record<string, unknown>, Record<never, never>>CartBadge =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anyspan>{
const cartCount: MarkedSignalAccessor
() => number (+1 overload)
cartCount
()}</IntrinsicElements[string]: anyspan>);

Both islands share the same cartCount signal. Clicking the button in CartButton updates the badge in CartBadge without any wiring between them.

Using signals in bind: bindings

Pass a signal directly into a bind: attribute to sync a form element with module-level state:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha, { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
} from "ilha"; const const query: SignalAccessor<string>query = signal<string>(initial: string): SignalAccessor<string>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
("");
const const SearchInput: Island<Record<string, unknown>, Record<never, never>>SearchInput =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(
() => <IntrinsicElements[string]: anyinput type: stringtype="search" bind:value: SignalAccessor<string>bind:bind:value: SignalAccessor<string>value={const query: SignalAccessor<string>query} />, ); const const SearchResults: Island<Record<string, unknown>, Record<never, never>>SearchResults =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => (
<IntrinsicElements[string]: anyp>Results for: {
const query: MarkedSignalAccessor
() => string (+1 overload)
query
()}</IntrinsicElements[string]: anyp>
));

When the user types, query updates and SearchResults re-renders automatically — no wiring between islands needed.


context(key, initial)

Creates a named global signal — a reactive signal shared across all islands. Identical keys always return the same signal instance, which makes it useful for app-wide singletons (theme, locale, current user) where you want registry semantics.

import { const context: <T>(key: string, initial: T) => ContextSignal<T>context } from "ilha";

const const theme: ContextSignal<string>theme = context<string>(key: string, initial: string): ContextSignal<string>context("app.theme", "light");

const theme: () => string (+1 overload)theme(); // → "light"
const theme: (value: string) => void (+1 overload)theme("dark"); // → sets to "dark"

signal() vs context()

Both return the same accessor shape and can be used with bind: template syntax. Reach for signal() when you hold the reference yourself and import it where needed. Reach for context() when you want a name-keyed registry so the same signal can be looked up from anywhere by string key — for example, when the consumer lives in a different package or module from where the signal is defined.

Sharing state between islands

Any island that calls context() with the same key gets the same signal. When one island writes to it, all others that read it re-render automatically:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha, { const context: <T>(key: string, initial: T) => ContextSignal<T>context } from "ilha"; const const cartCount: ContextSignal<number>cartCount = context<number>(key: string, initial: number): ContextSignal<number>context("cart.count", 0); const const CartButton: Island<Record<string, unknown>, Record<never, never>>CartButton =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, Record<never, never>, "click", Record<never, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>on("button@click", () => const cartCount: (value: number) => void (+1 overload)cartCount(const cartCount: () => number (+1 overload)cartCount() + 1)) .IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anybutton>Add to cart</IntrinsicElements[string]: anybutton>); const const CartBadge: Island<Record<string, unknown>, Record<never, never>>CartBadge =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anyspan>{const cartCount: () => number (+1 overload)cartCount()}</IntrinsicElements[string]: anyspan>);

Using context in bind: bindings

Pass a context signal directly into a bind: attribute to sync a form element across islands:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha, { const context: <T>(key: string, initial: T) => ContextSignal<T>context } from "ilha"; const const query: ContextSignal<string>query = context<string>(key: string, initial: string): ContextSignal<string>context("search.query", ""); const const SearchInput: Island<Record<string, unknown>, Record<never, never>>SearchInput =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(
() => <IntrinsicElements[string]: anyinput type: stringtype="search" bind:value: ContextSignal<string>bind:bind:value: ContextSignal<string>value={const query: ContextSignal<string>query} />, ); const const SearchResults: Island<Record<string, unknown>, Record<never, never>>SearchResults =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => (
<IntrinsicElements[string]: anyp>Results for: {const query: () => string (+1 overload)query()}</IntrinsicElements[string]: anyp> ));

Initializing with a type

The second argument sets the initial value and infers the signal type. The type is fixed at first call — subsequent calls with the same key return the existing signal regardless of what initial value is passed:

import { const context: <T>(key: string, initial: T) => ContextSignal<T>context } from "ilha";

const const count: ContextSignal<number>count = context<number>(key: string, initial: number): ContextSignal<number>context("ui.count", 0); // creates signal<number>
const const same: ContextSignal<number>same = context<number>(key: string, initial: number): ContextSignal<number>context("ui.count", 999); // returns same signal, ignores 999

This means context initialization is effectively first-write-wins. Define context signals in a shared module to ensure consistent initialization across your app:

// contexts.ts
import { context } from "ilha";

export const theme = context("app.theme", "light");
export const userId = context(
  "app.userId",
  null as string | null,
);
export const sidebar = context("ui.sidebar", true);

Reading context inside effects and derived

Context signals are reactive — reading them inside .effect() or .derived() creates a dependency just like reading local state:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha, { const context: <T>(key: string, initial: T) => ContextSignal<T>context } from "ilha"; const const theme: ContextSignal<string>theme = context<string>(key: string, initial: string): ContextSignal<string>context("app.theme", "light"); const const Island: Island<Record<string, unknown>, Record<never, never>>Island =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.effect(fn: (ctx: EffectContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => (() => void) | void): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>effect(() => { var document: Document
**`window.document`** returns a reference to the document contained in the window. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.documentElement: HTMLElement
The **`documentElement`** read-only property of the Document interface returns the Element that is the root element of the document (for example, the <html> element for HTML documents). [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/documentElement)
documentElement
.HTMLOrSVGElement.dataset: DOMStringMap
[MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dataset)
dataset
["theme"] = const theme: () => string (+1 overload)theme();
}) .IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anydiv>content</IntrinsicElements[string]: anydiv>);

Whenever theme is updated anywhere in the app, this effect re-runs.

SSR behavior

context() is safe to call during SSR. The registry is module-level, so signals persist for the lifetime of the process. In a server environment where requests share the same module instance, be careful not to store user-specific state in context signals — use .input() and .state() for per-request data instead.


batch(fn)

Runs fn as an atomic batch — multiple signal writes inside the callback produce a single propagation pass, so dependents (effects, deriveds, island re-renders) see the final state and run once instead of once per write. Returns whatever fn returns.

Before and after

Without batch, each write triggers its own propagation pass:

import { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
} from "ilha";
const const a: SignalAccessor<number>a = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
const const b: SignalAccessor<number>b = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
const a: MarkedSignalAccessor
(value: number) => void (+1 overload)
a(1); // → effects re-run
const b: MarkedSignalAccessor
(value: number) => void (+1 overload)
b
(2); // → effects re-run again

With batch, both writes flush together:

import { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
, function batch<T>(fn: () => T): T
Run `fn` as an atomic batch — multiple signal writes inside the callback produce a single propagation pass, so dependents (effects, deriveds, island re-renders) see the final state and run once instead of once per write. Returns whatever `fn` returns. Note: `.on()` handlers and `.effect()` runs are batched implicitly, so you only need this when triggering multiple writes from outside an island (e.g. from a top-level event listener or async callback).
batch
} from "ilha";
const const a: SignalAccessor<number>a = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
const const b: SignalAccessor<number>b = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
batch<void>(fn: () => void): void
Run `fn` as an atomic batch — multiple signal writes inside the callback produce a single propagation pass, so dependents (effects, deriveds, island re-renders) see the final state and run once instead of once per write. Returns whatever `fn` returns. Note: `.on()` handlers and `.effect()` runs are batched implicitly, so you only need this when triggering multiple writes from outside an island (e.g. from a top-level event listener or async callback).
batch
(() => {
const a: MarkedSignalAccessor
(value: number) => void (+1 overload)
a(10);
const b: MarkedSignalAccessor
(value: number) => void (+1 overload)
b
(20);
}); // → effects re-run once

Implicit batching

.on() handlers and .effect() runs are batched implicitly, so you only need batch() when triggering multiple writes from outside an island — for example from a top-level event listener, a setTimeout callback, or a WebSocket message handler.

Nesting

Nested batch() calls are safe and only flush when the outermost batch ends:

import { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
, function batch<T>(fn: () => T): T
Run `fn` as an atomic batch — multiple signal writes inside the callback produce a single propagation pass, so dependents (effects, deriveds, island re-renders) see the final state and run once instead of once per write. Returns whatever `fn` returns. Note: `.on()` handlers and `.effect()` runs are batched implicitly, so you only need this when triggering multiple writes from outside an island (e.g. from a top-level event listener or async callback).
batch
} from "ilha";
const const count: SignalAccessor<number>count = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
batch<void>(fn: () => void): void
Run `fn` as an atomic batch — multiple signal writes inside the callback produce a single propagation pass, so dependents (effects, deriveds, island re-renders) see the final state and run once instead of once per write. Returns whatever `fn` returns. Note: `.on()` handlers and `.effect()` runs are batched implicitly, so you only need this when triggering multiple writes from outside an island (e.g. from a top-level event listener or async callback).
batch
(() => {
batch<void>(fn: () => void): void
Run `fn` as an atomic batch — multiple signal writes inside the callback produce a single propagation pass, so dependents (effects, deriveds, island re-renders) see the final state and run once instead of once per write. Returns whatever `fn` returns. Note: `.on()` handlers and `.effect()` runs are batched implicitly, so you only need this when triggering multiple writes from outside an island (e.g. from a top-level event listener or async callback).
batch
(() => {
const count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count(1); }); // still inside outer batch — no flush yet
const count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count
(2);
}); // outermost batch ends — single flush

untrack(fn)

Runs fn with reactive tracking suspended. Reading signals inside fn returns their current value without subscribing the surrounding scope. Use this in effects or deriveds when you want to peek at state without causing a re-run on its changes.

React to A, peek at B

The canonical pattern: an effect should re-run when tracked changes, but read peeked only as a one-off value:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha, { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
, function untrack<T>(fn: () => T): T
Run `fn` with reactive tracking suspended. Reading signals inside `fn` returns their current value without subscribing the surrounding scope. Use this in effects/deriveds when you want to peek at state without causing a re-run on its changes.
untrack
} from "ilha"; const const tracked: SignalAccessor<number>tracked = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(0);
const const peeked: SignalAccessor<string>peeked = signal<string>(initial: string): SignalAccessor<string>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
("hello");
const const Island: Island<Record<string, unknown>, Record<never, never>>Island =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.effect(fn: (ctx: EffectContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => (() => void) | void): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>effect(() => { // Re-runs when `tracked` changes, but NOT when `peeked` changes. var console: Consoleconsole.Console.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
const tracked: MarkedSignalAccessor
() => number (+1 overload)
tracked
(),
untrack<string>(fn: () => string): string
Run `fn` with reactive tracking suspended. Reading signals inside `fn` returns their current value without subscribing the surrounding scope. Use this in effects/deriveds when you want to peek at state without causing a re-run on its changes.
untrack
(() =>
const peeked: MarkedSignalAccessor
() => string (+1 overload)
peeked
()),
); }) .IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anyp>x</IntrinsicElements[string]: anyp>);

untrack() returns whatever fn returns, so it also works for peeking at derived values or any other reactive read:

import { function signal<T>(initial: T): SignalAccessor<T>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
, function untrack<T>(fn: () => T): T
Run `fn` with reactive tracking suspended. Reading signals inside `fn` returns their current value without subscribing the surrounding scope. Use this in effects/deriveds when you want to peek at state without causing a re-run on its changes.
untrack
} from "ilha";
const const s: SignalAccessor<number>s = signal<number>(initial: number): SignalAccessor<number>
Create a free-standing reactive signal that lives outside any island. Useful for sharing state across islands without prop drilling, or for binding form inputs to module-level state via the `bind:value=${signal}` template syntax. The returned accessor is a getter when called with no arguments and a setter when called with one. Reading it inside a `.derived()`, `.effect()`, or `.render()` automatically subscribes the surrounding reactive scope — so when the signal changes, dependents re-run as if it were local state.
signal
(42);
const const value: numbervalue = untrack<number>(fn: () => number): number
Run `fn` with reactive tracking suspended. Reading signals inside `fn` returns their current value without subscribing the surrounding scope. Use this in effects/deriveds when you want to peek at state without causing a re-run on its changes.
untrack
(() =>
const s: MarkedSignalAccessor
() => number (+1 overload)
s()); // → 42, no subscription created

Notes

  • signal() vs context() — both return the same accessor shape and can be used with bind: template syntax. Use signal() for one-off shared state where you hold the reference; use context() when you want a name-keyed registry.
  • Keys are global strings. Use namespaced keys like "app.theme" or "cart.count" to avoid accidental collisions across different parts of your app.
  • There is no way to delete or reset a context signal once created short of reloading the module.
  • Context signals are not included in .hydratable() snapshots. If you need server-rendered context values on the client, pass them as island props via .input() and initialize the context signal inside .onMount().