Components

Effex components are just functions that return Elements. There’s no special component class, no hooks rules, and no lifecycle methods. A component is either a plain function (for static content) or an Effect generator (for state and context).

Simple Components

Components without state or context requirements are plain functions:

import { $, collect } from "@effex/dom";

const Greeting = (props: { name: string }) =>
  $.h1({}, $.of(`Hello, ${props.name}!`));

With Children

Use generics on E and R to propagate error and requirement types from children:

import { type Element } from "@effex/dom";

const Card = <E, R>(
  props: { title: string },
  children: Element.Child<E, R>,
) =>
  $.div({ class: "card" }, collect(
    $.h2({}, $.of(props.title)),
    children,
  ));

This ensures that if the children require a context or may produce an error, those types flow through to the Card’s return type. The compiler tracks them for you.

Stateful Components

Use Effect.gen when you need signals, context, or other Effects:

import { Effect } from "effect";
import { $, collect, Signal } 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("+"),
      ),
    ));
  });

The yield* is where Effects are executed. Signal.make creates a scoped signal, $.div creates a DOM element — both are Effects, so they compose naturally.

Accessing Context

Components that depend on context simply yield* the context tag:

const UserBadge = () =>
  Effect.gen(function* () {
    const user = yield* UserContext;
    return yield* $.span({}, $.of(user.name));
  });

The UserContext requirement appears in the component’s R type channel. If you try to render UserBadge without providing UserContext, TypeScript catches it at compile time.

Context Providers

Use provide to supply context to children:

import { Context, Effect } from "effect";
import { $, provide } from "@effex/dom";

class ThemeContext extends Context.Tag("ThemeContext")<
  ThemeContext,
  Theme
>() {}

const ThemedButton = (props: { label: string }) =>
  Effect.gen(function* () {
    const theme = yield* ThemeContext;
    return yield* $.button(
      { style: { backgroundColor: theme.primary } },
      $.of(props.label),
    );
  });

// Provide context to children
$.div({},
  provide(ThemeContext, myTheme,
    ThemedButton({ label: "Click" }),
  ),
);

provide removes ThemeContext from the R channel — downstream code no longer needs to satisfy that requirement.

Running Your App

Use runApp and mount to start your application:

import { Effect } from "effect";
import { mount, runApp } from "@effex/dom";

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

runApp handles boilerplate: scoping, the signal registry, and keeping the process alive. You can pass additional layers:

import { Navigation } from "@effex/router";

runApp(
  Effect.gen(function* () {
    yield* mount(App(), document.getElementById("root")!);
  }),
  { layer: Navigation.makeLayer(router) },
);