effract docs GitHub ↗

Getting started

Install effract and render your first React Effect Component.

effract lets you write React components as Effect programs. A component body is a generator that effract interprets inside React’s render pass, so it can yield* both Effect services and real React hooks. The same component then runs in a SPA, on a server, in a Web Worker, or as a React Server Component.

Install

npm i @tmonier/effract

effract requires React 19.2+ and Effect v4 (installed automatically as peers). For the RSC renderer, also add @tmonier/effract-rsc.

Incremental — not a rewrite

effract does not ask you to rewrite your app. There are two kinds of component, and the distinction matters:

The two compose freely in the same tree. You convert a component to a REC only when it actually reaches for a service — never wholesale.

Your first component

A React Effect Component (REC) is a real React component. Define a service, read it in a REC, and wire the runtime in once with mount:

import { mount, rec, hook } from '@tmonier/effract';
import { createRoot } from 'react-dom/client';
import { useState } from 'react';
import * as Context from 'effect/Context';
import * as Layer from 'effect/Layer';

class Stats extends Context.Service<Stats, { total: number }>()('app/Stats') {}
const StatsLive = Layer.succeed(Stats)({ total: 1280 });

// Card is a plain React component — an ordinary function, left untouched.
const Card = ({ children }: { children: React.ReactNode }) => <section className="card">{children}</section>;

// Counter is a REC: it reaches for a service, so it's written with `rec(...)`.
const Counter = rec(function* () {
  const stats = yield* Stats; // an Effect service
  const [n, setN] = yield* hook(useState(0)); // a real React hook
  return (
    <button onClick={() => setN(n + 1)}>
      {n} · {stats.total} total
    </button>
  );
});

// App places the plain Card as JSX and the Counter REC by yielding it.
const App = rec(function* () {
  return <Card>{yield* Counter}</Card>;
});

createRoot(document.getElementById('root')!).render(mount(StatsLive, App));

A REC is not a JSX element — <Counter /> is a compile error. You place a REC by yield*-ing it inside another component’s returned JSX ({yield* Counter}, or {yield* Counter.with({ ... })} to pass props), while plain components like <Card> stay normal JSX. mount(StatsLive, App) builds the Effect runtime once and returns a ReactNode; it verifies at compile time that the layer provides every service the tree needs — a missing service is a type error that names it. The Stats service is then resolved synchronously inside Counter.

Where to next