Reactive UI, woven from Effect.
Frontend at scale is hard. Real applications need robust API orchestration, error handling, retries, and telemetry. Effect solves these problems elegantly, and Weft brings the same patterns to the UI layer, in the browser and on the server.
Weft is a reactive DOM library where every node is an Effect. Components are plain functions that return Node<E, R>, a type alias for Effect.Effect<ElementDescriptor, E, R>. Error and requirement channels accumulate through the tree, and every Effect combinator applies to nodes directly.
Your component tree is the warp, the fixed structure held under tension; streams are the weft, the live thread drawn across it. Streams drive all updates, so there is no virtual DOM and no diffing.
The same tree renders to HTML on the server and hydrate()s in place on the client, flash-free. No JSX. The full model is explained in The Rendering Model.
Early Development Notice: Weft is in active early development. APIs may change rapidly. Not recommended for production use yet.
- Effect-first architecture: Services, Layers, and dependency injection across client and server
- Combinator API: Build trees with
h,h.fragment, andComponent.gen/Component.make, with no JSX and no build-tool plugins - Type-safe channels: Effect's
EandRchannels propagate through the full component tree - Ephemeral components: Components run once, streams drive all updates
- Error boundaries: Six failure-catch
Boundary.*variants, plusBoundary.suspendand theBoundary.rpcserver-data seam - SSR + Hydration:
renderToString,renderToStream, and flash-freehydrate()for full-stack apps - Progressive streaming:
renderToStreamemits HTML chunks in document order as slow nodes resolve - Universal routing:
@weftui/routermaps a URL to a nested page tree on both server and client, with type-safe params and persistent layouts
Weft is a monorepo with three packages:
@weftui/core: Element builders and combinators. Exportsh(withh.fragment),Component(Component.gen/Component.make),Boundary(six failure-catch variants,Boundary.suspend,Boundary.rpc),List(List.each), and theNode<E, R>/Source<A, E, R>types.@weftui/dom: The renderer.WeftApp.mountandWeftApp.hydratefor the browser (@weftui/dom/client);renderToString,renderToStringHydratable,renderToStream, andrenderToStreamHydratablefor the server (@weftui/dom/server).@weftui/router: Universal nested router. Authors a route tree withRouter.route/Router.layout/Router.router, splits it withRouter.lazy, and renders it on the server (@weftui/router/server) and the client (@weftui/router/client), with type-safehrefs and dependency-injected params.
npm install @weftui/core @weftui/dom @weftui/router effect@rcWeft tracks Effect 4's prerelease line (beta, then rc). This release is built and tested against effect@4.0.0-rc.112; the peer range accepts newer 4.0 prereleases, which may contain upstream breaking changes.
New to Effect? Check out the Effect documentation to learn the fundamentals. These docs assume you know them.
import { h } from "@weftui/core";
import { WeftApp } from "@weftui/dom/client";
import { Effect, SubscriptionRef } from "effect";
const Counter = () =>
Effect.gen(function* () {
const count = yield* SubscriptionRef.make(0);
return yield* h.div([
h.span([SubscriptionRef.changes(count)]),
h.button({ onclick: () => SubscriptionRef.update(count, (n) => n + 1) }, "+"),
h.button({ onclick: () => SubscriptionRef.update(count, (n) => n - 1) }, "-"),
]);
});
const app = WeftApp.make();
void Effect.runPromise(WeftApp.mount(app, Counter(), document.getElementById("root")!));Full documentation lives in docs/. Pick your entry point by what you are trying to do:
| Tutorial | Learning-oriented. A four-step guided path: your first app → reactivity → services and async → errors and server rendering. |
| How-to guides | Task-oriented. Author components, render on the server, load data with rpc, add routing, plus recipes for forms, keyed lists, refs, and more. |
| Explanation | Understanding-oriented. The rendering model, the combinator API, reactive primitives, boundaries, and services & context. |
| Reference | Information-oriented. Full API: @weftui/core, @weftui/dom, @weftui/router. |
New to the model itself? Read The Rendering Model: why there is no virtual DOM, and what "streams are the weft" means.
The examples/ directory contains standalone applications you can run with vp run -F <name> dev. Each covers a specific pattern and ships with a browser test:
| Example | What it shows |
|---|---|
async-data-loading |
Loading states, retry, error boundaries with Stream and Effect |
declarative-event-handlers |
Plain, Effect-returning, service-aware, and reactive handlers |
element-ref |
DOM refs with SubscriptionRef<Option<HTMLElement>> |
error-boundary |
All six failure-catch Boundary.* variants |
form-handling |
Reactive inputs, Schema validation, Effect submit handlers |
keyed-list |
Keyed list rendering with List.each |
list-rendering |
Static and stream-based lists, fragments, nested iterables |
reactive-styles |
Per-property and whole-object stream styles, CSS transitions |
router-ssr |
Universal nested routing with SSR, hydration, layouts, Boundary.rpc, Router.lazy |
server-boundary |
Boundary.rpc client-first mount + refetch, router-less |
ssr-hydration |
SSR + hydration without server data loading |
subscription-ref |
Local state, derived streams, coordinating multiple refs |
suspense |
Suspense boundaries for streaming SSR and client coordination |
type-augmentation |
Typed custom elements on h via the CustomElements interface |
The root vite.config.ts defines tasks you run with vp run <task>:
vp install # Install all workspace dependencies
vp run dev # Run dev recursively across workspace packages (vp run -r dev)
vp run pack # Build all packages
vp run check # Format, lint, and typecheck (packs first)
vp run test # Run all node/jsdom tests (packs first)
vp run test:browser # Run real-browser e2e tests via Playwright (packs first)To work on a single example:
vp run -F ssr-hydration devMIT