Every element carries its error and dependency types. TypeScript catches unhandled failures and missing context at compile time — not in production.
The same signals, components, and router work across SPAs, server-rendered apps, and static sites. One model from prototype to production.
Structured concurrency, typed errors, dependency injection, and automatic resource cleanup — all built in. No extra libraries required.
import { Effect } from "effect";
import { $, collect, Signal, mount, runApp } from "@effex/dom";
const Counter = () =>
Effect.gen(function* () {
const count = yield* Signal.make(0);
return yield* $.div(
{ class: "flex items-center gap-4" },
collect(
$.button(
{
class: "btn btn-primary",
onClick: () => count.update((n) => n - 1),
},
$.of("-"),
),
$.span({ class: "text-2xl tabular-nums" }, count),
$.button(
{
class: "btn btn-primary",
onClick: () => count.update((n) => n + 1),
},
$.of("+"),
),
),
);
});
// Run the app!
runApp(mount(Counter(), document.getElementById("root")!));
Signals are mutable references that track their own subscribers. Read a signal inside an element, and that element updates when the signal changes — automatically. No dependency arrays to maintain, no useCallback to remember, no stale closure bugs to chase down.
// Signals are references, not snapshots.
// No stale closures, no dependency arrays.
const name = yield* Signal.make("world");
// Use a signal directly as element content —
// the text node updates when name changes.
const greeting = yield* $.h1({}, name);
// Derived values update automatically.
const upper = Readable.map(name, (n) => n.toUpperCase());
const shout = yield* $.p({}, upper);
Every element in Effex has the type Element<E, R> — where E is the error channel and R is the required context. If a component can fail, TypeScript tells you before you ship. If it needs a service, the compiler asks for it. Runtime surprises become compile-time conversations.
// This component can fail — the error type says so.
const UserProfile = (id: string): Element<HttpError, ApiClient> =>
Effect.gen(function* () {
const api = yield* ApiClient;
const user = yield* api.getUser(id);
return yield* $.div({}, $.of(user.name));
});
// TypeScript won't let you mount this without
// handling HttpError and providing ApiClient.
// Errors are visible in the types, not hidden at runtime.
Write your components once. Run them client-side as an SPA, server-render with hydration, or pre-render as a static site. The same router, the same signals, the same component model — just a different entry point.
// Same component, three targets.
// SPA — client-side only
runApp(mount(App(), root));
// SSR — server renders, client hydrates
// server:
const routes = Platform.toHttpRoutes(router, opts);
// client:
hydrate(App(), root);
// SSG — pre-render at build time
Route.static({
paths: () => discoverPages(),
load: ({ params }) => loadPage(params.slug),
render: (data) => DocPage(data),
});
A complete set of packages that work together — or independently.
Reactive primitives — Signal, Readable, reactive collections, control flow, and transitions.
DOM rendering with the $ factory, animations, portals, virtual lists, and hydration.
Type-safe routing with schema-validated params, data loaders, and mutation handlers.
Schema-first forms with per-field reactivity, validation, and nested structures.
Full-stack SSR integration with @effect/platform — server rendering, data serialization, and hydration.
Vite plugin for SSR dev server, server-code stripping, and static site generation.