effex-monorepo
    Preparing search index...

    effex-monorepo

    Effex

    A reactive UI framework built on Effect. Effex provides a declarative way to build web interfaces with fine-grained reactivity, automatic cleanup, and full type safety.

    Effex brings the power of Effect to frontend development. If you're building with Effect, this is a UI framework that speaks the same language.

    Every element has type Element<E, R> where E is the error channel. Errors propagate through the component tree, and you must handle them before mounting:

    // This won't compile — UserProfile might fail with ApiError
    mount(UserProfile(), document.body); // Type error!

    // Handle the error first
    mount(
    Boundary.error(
    () => UserProfile(),
    (error) => $.div({}, $.of(`Failed to load: ${error.message}`)),
    ),
    document.body,
    ); // Compiles

    TypeScript tells you at build time which components can fail and forces you to handle it.

    Effex uses signals for reactive state. When a signal updates, only the DOM nodes that depend on it update. No virtual DOM, no diffing, no wasted work:

    const Counter = () =>
    Effect.gen(function* () {
    const count = yield* Signal.make(0);
    console.log("setup"); // Logs once, on mount
    return yield* $.div({}, $.of(count)); // count changes update only this text node
    });

    Effex uses Effect's scope system. Subscriptions, timers, and other resources are automatically cleaned up when components unmount:

    yield* eventSource.pipe(
    Stream.runForEach(handler),
    Effect.forkIn(scope), // Cleaned up when scope closes
    );

    Effex gives you access to Effect's entire ecosystem:

    • Schema — Runtime validation with static types
    • Streams — Reactive data flows
    • Services — Dependency injection via Effect's context system
    • Retry/timeout — Built-in resilience patterns
    • Structured concurrency — Fork, join, and race without footguns
    # Create a new project
    pnpm create effex my-app
    cd my-app
    pnpm install
    pnpm dev

    Or install packages individually:

    # SPA (client-side only)
    pnpm add @effex/dom @effex/router effect

    # Full-stack SSR
    pnpm add @effex/dom @effex/router @effex/platform @effect/platform effect

    @effex/dom re-exports everything from @effex/core, so you don't need to install core separately.

    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(
    {},
    collect(
    $.button({ onClick: () => count.update((n) => n - 1) }, $.of("-")),
    $.span({}, $.of(count)),
    $.button({ onClick: () => count.update((n) => n + 1) }, $.of("+")),
    ),
    );
    });

    runApp(
    Effect.gen(function* () {
    yield* mount(Counter(), document.getElementById("root")!);
    }),
    );

    Effex's reactivity layer lives in @effex/core (re-exported by @effex/dom):

    import { Effect } from "effect";
    import { Signal, Readable, Ref } from "@effex/dom";

    // Mutable reactive state
    const count = yield* Signal.make(0);
    yield* count.set(5);
    yield* count.update((n) => n + 1);

    // Derived values (read-only, auto-tracked)
    const doubled = Readable.map(count, (n) => n * 2);
    const label = Readable.map(count, (n) => `Count: ${n}`);

    // Reactive collections
    const todos = yield* Signal.Array.make([{ text: "Learn Effex", done: false }]);
    yield* todos.push({ text: "Build something", done: false });

    const users = yield* Signal.Map.make(new Map([["alice", { name: "Alice" }]]));
    yield* users.set("bob", { name: "Bob" });

    // Reactive structs (each field is independently reactive)
    const form = yield* Signal.Struct.make({ name: "", email: "" });
    yield* form.name.set("Alice"); // Only updates subscribers of `name`

    // Lightweight mutable refs (not reactive, no subscriptions)
    const cache = yield* Ref.make(new Map());

    The @effex/dom package provides element constructors and reactive control flow:

    import { $, collect, each, when, matchOption, Readable } from "@effex/dom";

    // Elements accept reactive attributes
    $.input({
    class: Readable.map(hasError, (err) => err ? "input error" : "input"),
    value: name,
    onInput: (e) => name.set((e.target as HTMLInputElement).value),
    });

    // Conditional rendering
    when(isLoggedIn, {
    onTrue: () => Dashboard(),
    onFalse: () => LoginPage(),
    });

    // List rendering with keyed reconciliation
    each(todos, {
    key: (todo) => todo.id,
    render: (todo) => TodoItem({ todo }),
    });

    // Option matching
    matchOption(maybeUser, {
    onSome: (user) => UserCard({ user }),
    onNone: () => $.span({}, $.of("No user")),
    });

    @effex/router provides type-safe routing with the builder pattern:

    import { Route, Router, Outlet, Link } from "@effex/router";
    import { Schema } from "effect";

    // Define routes
    const HomeRoute = Route.make("/").pipe(
    Route.render(() => HomePage()),
    );

    const UserRoute = Route.make("/users/:id").pipe(
    Route.params(Schema.Struct({ id: Schema.String })),
    Route.render((data) => UserPage(data)),
    );

    // Compose into a router
    const router = Router.empty.pipe(
    Router.concat(HomeRoute),
    Router.concat(UserRoute),
    Router.fallback(() => NotFoundPage()),
    );

    // Render the matched route
    $.main({}, Outlet({ router }));

    // Navigate with type-safe links
    Link({ href: "/users/alice" }, $.of("Alice's Profile"));

    Routes can define server-side data loading and mutations when used with @effex/platform:

    import { Route } from "@effex/router";
    import { RedirectError } from "@effex/platform";

    const PostRoute = Route.make("/posts/:id").pipe(
    Route.params(Schema.Struct({ id: Schema.String })),

    // Loader: runs server-side with platform, client-side in SPA mode
    Route.get(
    ({ params }) =>
    Effect.gen(function* () {
    const svc = yield* PostService;
    return yield* svc.getPost(params.id);
    }),
    (post) => PostPage({ post }),
    ),

    // Mutation handlers: server-side only (via platform)
    Route.post("update", (body) =>
    Effect.gen(function* () {
    const svc = yield* PostService;
    return yield* svc.updatePost(body);
    }),
    ),
    );

    Route components access loader data and action endpoints via RouteDataContext:

    const { data, loaderPath, actions } = yield* RouteDataContext;
    

    @effex/form provides schema-validated forms with reactive field state:

    import { Field, Form } from "@effex/form";
    import { Schema } from "effect";

    // Define the form at module level
    const LoginForm = Form.make({
    email: Field.make(Schema.String.pipe(Schema.nonEmptyString()), { validateOn: "blur" }),
    password: Field.make(Schema.String.pipe(Schema.minLength(8)), { validateOn: "blur" }),
    });

    // Use in a component
    LoginForm.provide(
    {
    defaults: { email: "", password: "" },
    onSubmit: (ctx) => Effect.tryPromise(() => login(ctx.decoded)),
    },
    $.form(
    { class: "login" },
    collect(
    Effect.gen(function* () {
    const email = yield* LoginForm.fields.email;
    return yield* $.input({
    value: email.value,
    onInput: (e) => email.set((e.target as HTMLInputElement).value),
    onBlur: () => email.blur(),
    });
    }),
    // ... more fields
    ),
    ),
    );

    Supports leaf fields, nested structs, arrays, and maps — all with Effect Schema validation.

    @effex/platform bridges Effex with @effect/platform's HTTP server for server-side rendering:

    // server.ts
    import { Platform } from "@effex/platform";

    const effexRoutes = Platform.toHttpRoutes(router, {
    app: App,
    document: { title: "My App", scripts: ["/client.js"] },
    });

    // Compose with any @effect/platform HttpRouter
    const httpApp = HttpRouter.empty.pipe(
    HttpRouter.get("/api/health", HttpServerResponse.json({ ok: true })),
    HttpRouter.concat(effexRoutes),
    );
    // client.ts
    import { hydrate } from "@effex/dom/hydrate";
    import { Platform } from "@effex/platform";

    hydrate(App(), document.getElementById("root")!, {
    layers: Platform.makeClientLayer(router),
    });

    Key features:

    • SSR + Hydration — Server renders HTML, client picks up seamlessly
    • Loaders — Fetch data server-side, serialized to client for hydration
    • Mutation handlersRoute.post/put/delete execute server-side, return JSON
    • Data requests — Client navigations fetch data via ?_data=1 without full page loads
    • Redirects — Throw RedirectError from loaders for server-side redirects
    • HttpApi composition — Mount Effect's HttpApi alongside Effex pages on a single server
    Package Description
    @effex/core Reactive primitives: Signal, Readable, Ref, Signal.Array/Map/Struct, AsyncCache
    @effex/dom DOM rendering, elements, control flow, animation, mount/hydrate
    @effex/router Type-safe routing with loaders, mutation handlers, and Outlet
    @effex/form Schema-validated forms with reactive field state
    @effex/platform Server-side rendering, hydration, and data loading
    @effex/vite-plugin Vite plugin: SSR dev server + server-code stripping
    create-effex CLI to scaffold new projects (SPA or SSR)

    Import conventions:

    • @effex/dom re-exports everything from @effex/core — no need to install core separately
    • @effex/platform does not re-export dom or router — import them directly
    Example Description
    twitter Full-stack SSR app with loaders, mutations, and caching
    kanban Kanban board with drag-and-drop and forms
    todo-app Classic todo app
    router-demo Router features showcase

    Effex uses function calls instead of JSX:

    // Effex
    $.div(
    { class: "container" },
    collect($.h1({}, $.of("Hello")), $.p({}, $.of(count))),
    )

    Why:

    1. Error type preservation — Elements have type Element<E, R>. JSX would erase this to JSX.Element, losing type-safe error propagation.
    2. No build configuration — Works with any TypeScript setup. No JSX runtime, tsconfig tweaks, or bundler plugins.
    3. Explicit Effects — Every element is an Effect that must be yielded. JSX would obscure this.
    4. Consistent syntax — Components and elements use the same call pattern.

    Migration guides with concept mapping and side-by-side examples:

    • Effect — The foundation. Effect's typed errors, resource management, and structured concurrency inspired this entire project.
    • Solid — Fine-grained reactivity draws direct inspiration from Solid's reactive primitives.
    • TanStack — The router API is inspired by TanStack Router.
    • effect-form — The form package's schema-first, context-based architecture was inspired by this library.

    MIT