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:
| Helper | Purpose |
|---|---|
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: HTMLElementThe **`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): TRun `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): voidRun `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): TRun `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): voidRun `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): voidRun `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): TRun `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[]): voidThe **`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): stringRun `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): TRun `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): numberRun `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().