@tmonier/effract
Write React components as Effect programs. The same component — and the same mount — runs in a SPA,
during SSR, or as a React Server Component. One package, one import; the client/server split is chosen
by the bundler, never by you.
Docs & guide → effract.tmonier.com
Fastest start: npm create @tmonier/effract scaffolds effract into an existing app and teaches your
coding agent (via @tmonier/create-effract).
Version-matched docs ship in this package. After npm i, read
node_modules/@tmonier/effract/docs/ — start with patterns.md, then AGENTS.md. They always match
the installed version, so your (or your agent's) reference is never out of date.
import { mount, rec, hook } from '@tmonier/effract';
import { createRoot } from 'react-dom/client';
import { useState } from 'react';
const Dashboard = rec(function* () {
const stats = yield* Stats;
const [tab, setTab] = yield* hook(useState('overview'));
return <Panel tab={tab} total={stats.total} onTab={setTab} />;
});
createRoot(document.getElementById('root')!).render(mount(AppLive, Dashboard));
effract is incremental, not a rewrite. Plain React components stay exactly as they are (ordinary
<Component /> JSX). You write a REC with rec(...) only where a component reaches for the runtime,
and place one by yield*-ing it: {yield* Dashboard}, or {yield* Dashboard.with({ ... })} with props.
rec / view — hook-capable and resolve-up-front RECs. A REC is not a JSX element; place it
with {yield* Rec} inside another component's JSX.
hook — lift a React hook into the yield* channel.
mount(layer, RootRec) — the one boundary, client and server. Builds the Effect runtime once and
verifies at compile time that the layer provides every service the tree needs. In a React Server
Component graph the bundler's react-server condition gives it a server implementation (renders on the
server, no client JS); everywhere else it renders interactively. Same import in every file.
atom, observe, <Observe>, useAtom — the signals bridge (client).
renderToStream(layer, Root) — streaming SSR, on the server-only @tmonier/effract/ssr subpath
(entirely opt-in; a SPA never ships a byte of it). It embeds the settled state after the HTML and the
ordinary client mount seeds it automatically — no mismatch, no double fetch, no hydration API to
call. Mark wire-crossing state with atom.serializable(key, initial, schema).
See the project README and
ADR 0001.
MIT © Tmonier