Derived
Declares a computed value that depends on state or input. Derived values can be synchronous or async, and they re-run automatically when any reactive dependency changes.
Basic usage
Each derived entry is a signal accessor — call it with no arguments to read the resolved value, the same way you read state.count():
derived.total(); // read → returns current value
When any signal the derived function reads changes, the function re-runs and the island re-renders.
Reading and writing
Derived accessors can also be written for optimistic UI. A write updates the value immediately without waiting for the derived function to re-run:
derived.total(999); // write → sets value optimistically
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 from "ilha";
const const Cart: Island<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>>Cart = 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>>.state<number, "price">(key: "price", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "price", number>, Record<never, never>>state("price", 100)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "price", number>, Record<never, never>>.state<number, "qty">(key: "qty", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, Record<never, never>>state("qty", 3)
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, Record<never, never>>.derived<"total", number>(key: "total", fn: DerivedFn<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, number>): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, Record<never, never> & Record<"total", number>>derived("total", ({ state: IslandState<MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>>state }) => state: IslandState<MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>>state.price: MarkedSignalAccessor
() => number (+1 overload)
price() * state: IslandState<MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>>state.qty: MarkedSignalAccessor
() => number (+1 overload)
qty())
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, Record<never, never> & Record<...>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, "click", Record<never, never> & Record<"total", number>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, Record<never, never> & Record<...>>on("button@click", ({ derived: IslandDerived<Record<never, never> & Record<"total", number>>derived }) => derived: IslandDerived<Record<never, never> & Record<"total", number>>derived.total: (value: number) => void (+1 overload)total(0))
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, Record<never, never> & Record<...>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>, Record<never, never> & Record<"total", number>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<MergeState<Record<never, never>, "price", number>, "qty", number>>render(({ derived: IslandDerived<Record<never, never> & Record<"total", number>>derived }) => <IntrinsicElements[string]: anyp>Total: {derived: IslandDerived<Record<never, never> & Record<"total", number>>derived.total: () => number | undefined (+1 overload)total()}</IntrinsicElements[string]: anyp>);
The next time the derived function runs (for example after state changes or an async fetch resolves), it overwrites the optimistic value with the computed result.
The derived envelope
Every derived value also exposes loading, value, and error on the accessor itself. Use these when you need explicit control — especially with async derived values:
Property Type Description loadingbooleantrue while the function is runningvalueT | undefinedThe last successfully resolved value errorError | undefinedSet if the function threw or rejected
derived.total(); // same as derived.total.value when resolved
derived.total.value; // envelope read
derived.total.loading; // false for sync derived after first run
derived.total.error; // undefined when no error
For synchronous derived values, loading is false after the first run and () returns the computed value directly. For async derived values, check loading and error before reading value or calling ().
Async derived values
Pass an async function to fetch data or run any other asynchronous work. The envelope tracks progress while the promise is pending:
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 from "ilha";
const const UserCard: Island<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>>UserCard = 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>>.state<number, "userId">(key: "userId", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>, Record<never, never>>state("userId", 1)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>, Record<never, never>>.derived<"user", any>(key: "user", fn: DerivedFn<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>, any>): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>, Record<never, never> & Record<"user", any>>derived("user", async ({ state: IslandState<MergeState<Record<never, never>, "userId", number>>state, signal: AbortSignalsignal }) => {
const const res: Responseres = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch(`/api/users/${state: IslandState<MergeState<Record<never, never>, "userId", number>>state.userId: MarkedSignalAccessor
() => number (+1 overload)
userId()}`, {
RequestInit.signal?: AbortSignal | null | undefinedAn AbortSignal to set request's signal.signal,
});
return const res: Responseres.Body.json(): Promise<any>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)json();
})
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>, Record<never, never> & Record<"user", any>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>, Record<never, never> & Record<"user", any>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "userId", number>>render(({ derived: IslandDerived<Record<never, never> & Record<"user", any>>derived }) => {
if (derived: IslandDerived<Record<never, never> & Record<"user", any>>derived.user: DerivedAccessor<any>user.loading: booleanloading) return <IntrinsicElements[string]: anyp>Loading…</IntrinsicElements[string]: anyp>;
if (derived: IslandDerived<Record<never, never> & Record<"user", any>>derived.user: DerivedAccessor<any>user.error: Error | undefinederror)
return <IntrinsicElements[string]: anyp>Error: {derived: IslandDerived<Record<never, never> & Record<"user", any>>derived.user: DerivedAccessor<any>user.error: Errorerror.Error.message: stringmessage}</IntrinsicElements[string]: anyp>;
return <IntrinsicElements[string]: anyp>{derived: IslandDerived<Record<never, never> & Record<"user", any>>derived.user: () => any (+1 overload)user().name}</IntrinsicElements[string]: anyp>;
});
On first render, loading is true and derived.user() is undefined until the promise resolves.
Reactive dependencies
The derived function re-runs whenever any signal it reads changes. Dependencies are tracked automatically — you do not need to declare them manually.
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 from "ilha";
const const Search: Island<Record<string, unknown>, MergeState<Record<never, never>, "query", string>>Search = 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>>.state<string, "query">(key: "query", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "query", string>, Record<never, never>>state("query", "")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "query", string>, Record<never, never>>.derived<"results", string[]>(key: "results", fn: DerivedFn<Record<string, unknown>, MergeState<Record<never, never>, "query", string>, string[]>): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "query", string>, Record<never, never> & Record<"results", string[]>>derived("results", async ({ state: IslandState<MergeState<Record<never, never>, "query", string>>state, signal: AbortSignalsignal }) => {
const const res: Responseres = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch(`/api/search?q=${state: IslandState<MergeState<Record<never, never>, "query", string>>state.query: MarkedSignalAccessor
() => string (+1 overload)
query()}`, {
RequestInit.signal?: AbortSignal | null | undefinedAn AbortSignal to set request's signal.signal,
});
return const res: Responseres.Body.json(): Promise<any>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)json() as interface Promise<T>Represents the completion of an asynchronous operationPromise<string[]>;
})
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "query", string>, Record<never, never> & Record<"results", string[]>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "query", string>, Record<never, never> & Record<"results", string[]>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "query", string>>render(({ state: IslandState<MergeState<Record<never, never>, "query", string>>state, derived: IslandDerived<Record<never, never> & Record<"results", string[]>>derived }) => (
<>
<IntrinsicElements[string]: anyinput value: stringvalue={state: IslandState<MergeState<Record<never, never>, "query", string>>state.query: MarkedSignalAccessor
() => string (+1 overload)
query()} />
{derived: IslandDerived<Record<never, never> & Record<"results", string[]>>derived.results: DerivedAccessor<string[]>results.loading: booleanloading ? (
<IntrinsicElements[string]: anyp>Searching…</IntrinsicElements[string]: anyp>
) : (
<IntrinsicElements[string]: anyul>
{derived: IslandDerived<Record<never, never> & Record<"results", string[]>>derived.results: () => string[] | undefined (+1 overload)results()?.Array<string>.map<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any): U[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((r: stringr) => (
<IntrinsicElements[string]: anyli>{r: stringr}</IntrinsicElements[string]: anyli>
))}
</IntrinsicElements[string]: anyul>
)}
</>
));
Abort signal
Every async derived function receives an AbortSignal that aborts when the function is about to re-run. Pass it to fetch or any other cancellable API to avoid stale responses:
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 from "ilha";
const const Island: Island<Record<string, unknown>, MergeState<Record<never, never>, "id", number>>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>>.state<number, "id">(key: "id", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "id", number>, Record<never, never>>state("id", 1)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "id", number>, Record<never, never>>.derived<"data", any>(key: "data", fn: DerivedFn<Record<string, unknown>, MergeState<Record<never, never>, "id", number>, any>): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "id", number>, Record<never, never> & Record<"data", any>>derived("data", async ({ state: IslandState<MergeState<Record<never, never>, "id", number>>state, signal: AbortSignalsignal }) => {
const const res: Responseres = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch(`/api/items/${state: IslandState<MergeState<Record<never, never>, "id", number>>state.id: MarkedSignalAccessor
() => number (+1 overload)
id()}`, {
RequestInit.signal?: AbortSignal | null | undefinedAn AbortSignal to set request's signal.signal,
});
return const res: Responseres.Body.json(): Promise<any>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)json();
})
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "id", number>, Record<never, never> & Record<"data", any>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "id", number>, Record<never, never> & Record<"data", any>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "id", number>>render(({ derived: IslandDerived<Record<never, never> & Record<"data", any>>derived }) => (
<IntrinsicElements[string]: anyp>{derived: IslandDerived<Record<never, never> & Record<"data", any>>derived.data: () => any (+1 overload)data()?.name ?? "…"}</IntrinsicElements[string]: anyp>
));
If the signal was already aborted before your async work completes, the result is discarded silently.
Keeping stale value during reload
When a derived function re-runs, loading becomes true but value retains the previous result until the new one resolves. This lets you avoid layout shifts by showing stale content while refreshing:
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 from "ilha";
const const Island: Island<Record<string, unknown>, MergeState<Record<never, never>, "page", number>>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>>.state<number, "page">(key: "page", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "page", number>, Record<never, never>>state("page", 1)
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "page", number>, Record<never, never>>.derived<"items", string[]>(key: "items", fn: DerivedFn<Record<string, unknown>, MergeState<Record<never, never>, "page", number>, string[]>): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "page", number>, Record<never, never> & Record<"items", string[]>>derived("items", async ({ state: IslandState<MergeState<Record<never, never>, "page", number>>state, signal: AbortSignalsignal }) => {
const const res: Responseres = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch(`/api/items?page=${state: IslandState<MergeState<Record<never, never>, "page", number>>state.page: MarkedSignalAccessor
() => number (+1 overload)
page()}`, {
RequestInit.signal?: AbortSignal | null | undefinedAn AbortSignal to set request's signal.signal,
});
return const res: Responseres.Body.json(): Promise<any>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)json() as interface Promise<T>Represents the completion of an asynchronous operationPromise<string[]>;
})
.IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "page", number>, Record<never, never> & Record<"items", string[]>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "page", number>, Record<never, never> & Record<"items", string[]>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "page", number>>render(({ derived: IslandDerived<Record<never, never> & Record<"items", string[]>>derived }) => (
<>
<IntrinsicElements[string]: anyul
style: stringstyle={`opacity: ${derived: IslandDerived<Record<never, never> & Record<"items", string[]>>derived.items: DerivedAccessor<string[]>items.loading: booleanloading ? "0.5" : "1"}`}
>
{derived: IslandDerived<Record<never, never> & Record<"items", string[]>>derived.items: () => string[] | undefined (+1 overload)items()?.Array<string>.map<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any): U[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((i: stringi) => (
<IntrinsicElements[string]: anyli>{i: stringi}</IntrinsicElements[string]: anyli>
))}
</IntrinsicElements[string]: anyul>
<IntrinsicElements[string]: anybutton>Next page</IntrinsicElements[string]: anybutton>
</>
));
SSR behavior
During SSR, derived functions are called once. If they are async, the island awaits them before rendering when called as await island(props). When called synchronously via island.toString(), async derived values render with loading: true immediately.
// Async — waits for all derived values to resolve
const html = await MyIsland({ userId: 1 });
// Sync — derived renders in loading state
const html = MyIsland.toString({ userId: 1 });
Hydration snapshots
When using .hydratable() with snapshot: true, derived values are embedded in the server output and restored on the client. This means the island can render immediately on mount without re-fetching, using the server-resolved value as the initial state.
See .hydratable() for full snapshot options.
Notes
- Derived keys must be unique within the same builder chain.
- Rejected or thrown async derived work sets
derived.key.error on the envelope — it is not routed to .onError(). Handle failures in the render path via error / loading.
- Async schemas are not supported as derived functions — the function itself can be async, but ilha
.input() schemas must remain synchronous.
- Multiple derived entries are independent. Each tracks its own dependencies and re-runs on its own schedule.