@weftui/core API Reference
Element builders
h
Proxy-based namespace for building HTML and SVG elements. Every property is an element builder for that tag name:
import { h } from "@weftui/core";
h.div(props, children)
h.span(props, child: string | number)
h.input(props)
h.ul(children)
// ...any HTML or SVG tagEach builder has these overloads:
| Signature | Description |
|---|---|
h.tag(props, children: Renderable[]) |
Props + array of children |
h.tag(props, child: string | number) |
Props + single static child |
h.tag(props) |
Props only, no children |
h.tag(children: Renderable[]) |
Children only, no props |
h.tag(child: string | number) |
Single static child only |
h.tag() |
No arguments |
Return type: Node<PropsE<P> | ChildrenE<C>, PropsR<P> | ChildrenR<C>>
Reactive prop values (Stream, Effect, Subscribable) contribute their E/R to the node. Static values contribute never. Children's channels are unioned with props' channels.
h.fragment
Groups children into a fragment that renders without a wrapper element:
import { h } from "@weftui/core";
h.fragment(children: Node[]): Node<ChildrenE, ChildrenR>Use when a component needs to return multiple sibling elements.
Component
Namespace exposing two factories for reusable components with caller-propagating reactive prop types: Component.gen (generator body) and Component.make (plain-function body). Both return a callable that is generic over the caller's specific props/children, so reactive prop values and reactive children contribute their E/R at the call site.
import { Component } from "@weftui/core";
Component.gen<BaseProps, C>(
body: (props: BaseProps, children: C) => Generator<YieldedEffect, ElementDescriptor, never>
): <GenP extends BaseProps, GenC extends C>(
props: GenP,
children?: GenC,
) => Node<PropsE<GenP> | ChildrenE<…> | BodyE, PropsR<GenP> | ChildrenR<…> | BodyR>;
Component.make<BaseProps, C>(
body: (props: BaseProps, children: C) => Effect<ElementDescriptor, E, R>
): /* same call signature as above */;Children — the optional children argument may be either form:
type Component.Children<Input = never> =
| readonly Renderable[]
| ((input: Input) => readonly Renderable[]);For function-children, ChildrenE/ChildrenR are extracted from the function's ReturnType, not from the function itself. The component's body invokes the function with whatever input it chooses.
Example — Component.gen:
interface TextFieldProps {
value?: Source.Source<string>;
onChange?: (v: string) => void;
}
const TextField = Component.gen(function* (props: TextFieldProps) {
const value = yield* Source.toSubscribable(props.value);
return yield* h.input({
value,
oninput: (e) => props.onChange?.(e.currentTarget.value),
});
});Example — Component.make with function-children:
const Labeled = Component.make(
(props: { label: string }, children: (label: string) => readonly Renderable[]) =>
h.div({ class: "field" }, children(props.label)),
);
Labeled({ label: "Name" }, (label) => [h.label(label), h.input()]);For rendering a reactive collection, reach for the built-in
List.eachrather than mapping items by hand — it reconciles by key across emissions instead of rebuilding the region.
Boundary namespace
Variants for intercepting rendering-path errors in a subtree, plus Boundary.suspend for async fallbacks and Boundary.rpc for rpc-backed server data. Each returns a descriptor that the renderer processes via the same { type, props } branch. The catch variants share the same call shape — props first, children array second.
What is caught:
- Construction-time failures — the Effect phase of building child nodes
- Post-mount stream failures — streams driving children or prop values that fail after mount
What is NOT caught: event handler errors (they run in detached fibers outside the render path).
import { Boundary } from "@weftui/core";Boundary.suspend
Shows a fallback while async children are pending:
Boundary.suspend(
props: { fallback?: Renderable },
children: Node[]
): Node<ChildrenE, ChildrenR>- Shows
fallbackwhile any registered child has not yet emitted its first value - Performs a single atomic DOM swap once all children have settled
- Works on both the server (streaming patch model) and the client
hydrate()sees throughBoundary.suspendboundaries and adopts resolved DOM in place
Boundary.rpc
A universal server/client render boundary backed by one Rpc from the app's merged RpcGroup (@effect/rpc). The rpc _tag is the boundary's stable identity and its payload schema the typed input; the handler lives in the server-only rpc Layer (group.toLayer(...)), which the client never imports — tree-shaking does the client/server split structurally. Unlike the catch variants it takes a render function — not a children array — and that render receives a reactive Resource, not a bare value.
Boundary.rpc<R extends Rpc.Any, C extends Node<any, any>>(
rpc: R, // an Rpc from the merged RpcGroup; its _tag + schemas drive the boundary
payload: () => Rpc.Payload<R>, // thunk: a fresh typed payload per call (SSR / refetch / mount)
render: (resource: Resource<Rpc.Success<R>>) => C, // builds the subtree from a reactive Resource (not a bare value)
options?: { fallback?: Renderable }, // shown only during a client-first SPA mount
): Node<Node.Error<C> | Rpc.Error<R>, Node.Context<C>>The boundary resolves the rpc through the ambient AppRpcClientTag seam, provided by @weftui/router (RouterServer on the server, RouterLive on the client). It has four lifecycles:
- SSR: the server resolves the rpc in-process (over the handler Layer),
successSchema-encodes the result inline as<script type="application/json">at the region cursor, then rendersrender(seededResource)to HTML in place. - Hydrate:
hydratereads the inline payload positionally,successSchema-decodes it, seeds theResource, and adopts the DOM — it never re-calls the rpc (replay, not refetch). - Refetch:
resource.refetchcalls the rpc again over the network (POST /_eui/rpc) and patches the subtree in place (stale-on-error — a failed refetch leaves the previous value intact). - Client-first mount: SPA-navigating into a boundary with no SSR payload renders
options.fallback, forks the rpc call, and swaps inrender(resource)once it resolves.
Channel algebra: the output E is render's error union plus the rpc's typed Rpc.Error<R> (never for an rpc with no error schema). The output R is exactly render's R, untouched — there is no provide/RServer to discharge (the handler lives in the rpc Layer) and no Exclude is applied. A server-only tag accidentally referenced in render therefore stays in R, where hydrate's AssertNoServerOnly rejects it. Brand such services with ServerTag.
Typed-failure replay: a resolved rpc error on the SSR pass is errorSchema-encoded and relocated to the nearest enclosing failure Boundary, then replayed on the client (decoded and re-raised, reproducing the same fallback DOM — never retried). A transport defect, or an rpc with no error schema, is not replayed; it propagates.
Not yet covered: streamed success (
Rpc.make(..., { stream: true })) and mutations are on the roadmap, not this pass.
Resource<A>
The reactive handle render receives (A = Rpc.Success<R>). After hydrate the region is live: value is seeded with the SSR payload and the client can refetch the same data on demand, patching the rendered subtree in place.
| Field | Type | Meaning |
|---|---|---|
value |
Subscribable.Subscribable<A> |
Current data. Seeded with the SSR data (await-first, emits immediately, so SSR HTML and adopted DOM are byte-identical — no fallback flash). A successful refetch pushes the new value. |
refetch |
Effect.Effect<void> |
Re-resolves the rpc over the network with a fresh payload() and sets value. Client only — a no-op on the server. |
pending |
Subscribable.Subscribable<boolean> |
true while a refetch is in flight (false on the server / before any refetch). |
error |
Subscribable.Subscribable<Option<unknown>> |
Some with the last refetch error, else None. A failed refetch is stale-on-error — it does not unmount or raise into a failure Boundary. |
RpcOptions
interface RpcOptions {
readonly fallback?: Renderable; // shown only during a client-first mount; omit/null renders nothing while pending
}SERVER_BOUNDARY
export const SERVER_BOUNDARY: unique symbol;The descriptor type every Boundary.rpc carries ({ type: SERVER_BOUNDARY, props }). Exported for renderers, which detect and handle the boundary synchronously via the { type, props } branch without running the node.
AppRpcClientTag
interface AppRpcClient {
readonly call: (tag: string, payload: unknown) => Effect.Effect<unknown, unknown>;
}
class AppRpcClientTag extends Context.Tag("@weftui/core/AppRpcClient")<
AppRpcClientTag,
AppRpcClient
>() {}The ambient, package-neutral seam the renderer resolves a Boundary.rpc through — a flat, untyped caller (tag, payload) => Effect<success>. It lets @weftui/dom resolve a boundary without importing @effect/rpc or @weftui/router. @weftui/router provides it: a network RpcClient (POST /_eui/rpc) in the browser, an in-process client over the handler Layer on the server. call returns the already-decoded success; the renderer owns successSchema/errorSchema decoding of the inline SSR payload only. Both AppRpcClientTag and the AppRpcClient type are re-exported from @weftui/core. Absent in a router-less mount, where a Boundary.rpc resolves to a descriptive "needs router/rpc" error (not a defect).
See the rpc data boundaries guide and examples/router-ssr.
Boundary.catchAll
Catches all typed failures (Cause.fail). Defects (Cause.die) are not caught and re-raise.
Boundary.catchAll<C, FE, FR>(
props: { fallback: (e: ChildrenE<C>) => Node<FE, FR> },
children: C,
): Node<FE, ChildrenR<C> | FR>The children's E is fully consumed. The output E is only the fallback's own error channel.
Boundary.catchAllCause
Catches every Cause including defects and interruptions.
Boundary.catchAllCause<C, FE, FR>(
props: { fallback: (cause: Cause.Cause<ChildrenE<C>>) => Node<FE, FR> },
children: C,
): Node<FE, ChildrenR<C> | FR>The fallback receives the full Cause, not just the failure value.
Boundary.catchTag
Catches errors whose _tag equals props.tag. Unmatched errors re-raise to the nearest parent boundary.
Boundary.catchTag<C, Tag, FE, FR>(
props: {
tag: Tag; // must be a key of ChildrenE<C>["_tag"]
fallback: (e: Extract<ChildrenE<C>, { _tag: Tag }>) => Node<FE, FR>;
},
children: C,
): Node<Exclude<ChildrenE<C>, { _tag: Tag }> | FE, ChildrenR<C> | FR>The matched tag is removed from the output E union.
Boundary.catchTags
Catches multiple tags in one call. The handlers record IS the first argument (no wrapping object). Unregistered tags re-raise.
Boundary.catchTags<C, Handlers>(
handlers: {
[Tag in ChildrenE<C>["_tag"]]?: (e: Extract<ChildrenE<C>, { _tag: Tag }>) => Node<any, any>
},
children: C,
): Node<UnhandledE | HandlersE, ChildrenR<C> | HandlersR>Boundary.catchSome
The fallback returns Option<Node>. Option.none() re-raises the error; Option.some(node) catches it.
Boundary.catchSome<C, FE, FR>(
props: { fallback: (e: ChildrenE<C>) => Option.Option<Node<FE, FR>> },
children: C,
): Node<ChildrenE<C> | FE, ChildrenR<C> | FR>The children's E is preserved in the output because the boundary may or may not handle any given error.
Boundary.catchIf
A predicate gates the fallback. false re-raises.
Boundary.catchIf<C, FE, FR>(
props: {
predicate: (e: ChildrenE<C>) => boolean;
fallback: (e: ChildrenE<C>) => Node<FE, FR>;
},
children: C,
): Node<ChildrenE<C> | FE, ChildrenR<C> | FR>Re-raise and nesting
When a boundary's match returns null (unmatched error), the error propagates to the nearest parent Boundary via BoundaryContext. If there is no parent boundary, the error fails the enclosing mount.
Inner boundaries shadow outer ones for their subtree — the innermost boundary is always tried first.
// Inner catches FooError; BarError propagates to outer
Boundary.catchAll({ fallback: (e) => h.div(`Outer: ${e.message}`) }, [
Boundary.catchTag({ tag: "Foo", fallback: (e) => h.span(`Foo: ${e.msg}`) }, [
ChildWithFooOrBarError(),
]),
]);ServerTag
A Context.Tag whose identifier is branded server-only. Use it exactly like Context.Tag for services that must only ever be provided on the server — e.g. a database handle read inside an rpc handler Layer. The brand also guards Boundary.rpc: a server-only tag accidentally referenced in render stays in the requirement channel, where hydrate's AssertNoServerOnly rejects it at compile time.
import { ServerTag } from "@weftui/core";
import { Effect } from "effect";
class Database extends ServerTag("Database")<
Database,
{ readonly getProduct: () => Effect.Effect<Product> }
>() {}- The server-only brand rides along in the requirement channel
Rof any effect that uses the tag. - An rpc handler Layer (
group.toLayer(...)) discharges it on the server, where it is provided; it never enters aBoundary.rpc's outputR, sincerenderonly reads the decoded result. - If a branded tag ever reaches client code — referenced in
renderand surviving intohydrate's requirement channel —AssertNoServerOnlyresolvesRto a compile-error sentinel (ServerOnlyLeak) at thehydratecall site, rather than failing silently at runtime.
ServerOnly, ServerOnlyLeak, and AssertNoServerOnly<R> are exported alongside ServerTag for advanced typing; most code only needs ServerTag itself.
Keyed lists
List namespace
The keyed-list combinator. It is the opt-in alternative to wholesale child rebuilds: items are rendered once per key and reconciled across emissions, so reordering, inserting, or removing items reuses and moves existing DOM rather than rebuilding the region.
Note: This exported
Listnamespace is the built-in, key-reconciling way to render collections — prefer it over hand-rolling a component that maps items into elements.
import { h, List } from "@weftui/core";
h.ul([List.each({ of: rows.changes, by: (row) => row.id }, (row) => h.li(row.name))]);List.each
Declares a keyed reactive list region.
List.each<S extends Source.Source<Iterable<any>, any, any>, CE, CR, K>(
options: List.Options<S, K>,
render: (item: ItemOf<S>, index: number) => Node<CE, CR>,
): Node<Source.Error<S> | CE, Source.Context<S> | CR>render runs once per key; a persisted key keeps its DOM nodes and its running subscription fibers across re-emits (it is never re-invoked). The returned node's E/R are the union of the source channels and the channels of the node render returns.
⚠️ Render-once / index-key footgun: because
renderruns exactly once per key, reconciliation never refreshes a kept row's content — refresh a row by threading aStreaminside it, not by re-runningrender. Keying by index (by: (_, i) => i) reuses rows positionally and will show stale content after a reorder; prefer a stable identity key (by: (item) => item.id).
List.Options<S, K>
interface List.Options<S, K> {
readonly of: S; // static Iterable<T>, or an Effect/Stream/Subscribable of one
readonly by?: (item: ItemOf<S>, index: number) => K; // key projection; compared via Effect Equal / Hash
}of— the list source. Each emission is materialized to an array to fix order, then reconciled by key.by— projects each item to its reconciliation key. Omitted ⇒ the item itself is the key (structural forData, by reference otherwise).
List.Error<N> and List.Context<N>
Type-level accessors that extract the E and R channels from a list Node. Re-exported from the canonical Node.Error / Node.Context accessors.
See the examples/keyed-list example for a full reconciliation walkthrough (focus, uncontrolled inputs, and per-row counters surviving reorders).
Types
Node<E, R>
type Node<E = never, R = never> = Effect.Effect<ElementDescriptor, E, R>;The core tree type. Every element builder and component returns a Node. Because Node is an alias for Effect.Effect, all Effect operators work on nodes directly.
Source namespace
The Source namespace contains the reactive prop vocabulary type and its normalization utility:
import { Source } from "@weftui/core";
// The type union
type Source.Source<A, E, R> = A | Effect.Effect<A, E, R> | Stream.Stream<A, E, R> | Subscribable<A, E, R>Any prop or child that supports reactivity accepts a Source.Source. Static values, Effects, Streams, and Subscribables are all valid.
Source.toSubscribable(source, key?)
Normalizes any Source.Source into a hot Subscribable<A, E | NoPropValue, R> scoped to the enclosing Scope:
Source.toSubscribable<A, E, R>(
source: Source.Source<A, E, R>,
key?: string
): Effect.Effect<Subscribable<A, E | NoPropValue, R>, never, Scope>Normalization rules:
Subscribable→ returned by reference, no new ref or fiber- Static value →
getsucceeds immediately;changesemits once Effect→ memoized viaEffect.cached;changesemits the resolved value onceStream→ forks a scoped pump fiber that drains into aSubscriptionRef;getawaits the first emission
The pump fiber is tied to the enclosing scope via Effect.forkScoped — it terminates when the scope closes.
NoPropValue
Tagged error raised when a Stream prop ends before emitting a value:
class NoPropValue extends Data.TaggedError("NoPropValue")<{
readonly key?: string;
}> {}The key field identifies which prop triggered the error when provided.
PropsE<P> and PropsR<P>
Type-level utilities that extract the E and R channels from a props object:
type PropsE<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, infer E, any> ? E : ... }[keyof P]
type PropsR<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, any, infer R> ? R : ... }[keyof P]These are used internally by h and Component to accumulate channels from props. You generally don't need to reference them directly unless building utilities over the combinator API.
Constants
FRAGMENT
Internal brand used to mark fragment nodes. Not intended for direct use — use h.fragment instead.
Utility functions
isStream(value)
Returns true if value is a Stream.Stream:
isStream(value: unknown): value is Stream.Stream<unknown, unknown, unknown>toStream(value)
Normalizes a static value, Effect, or Stream into a Stream:
toStream<A>(value: A | Effect.Effect<A> | Stream.Stream<A>): Stream.Stream<A>- Static value →
Stream.make(value)(single emission) Effect→Stream.fromEffect(effect)(one-shot)Stream→ returned as-is
isSubscribable(value)
Returns true if value implements the Subscribable interface (keyed off Subscribable.TypeId):
isSubscribable(value: unknown): value is Subscribable<unknown, unknown, unknown>See also
- The Combinator API · Reactive Primitives · Boundaries and Suspense — the concepts behind this surface
- Author Components · Render Keyed Lists — task guides that use it
@weftui/domreference ·@weftui/routerreference