effract docs GitHub ↗

The thesis

How Effect and React fibers reconcile — 100% real React, no forked reconciler.

React schedules work on units it calls fibers. Effect schedules work on units it also calls fibers. effract is the loom between the two.

The body is a generator

A REC body is a plain generator — not an Effect. If it were an Effect.gen, it would run on an Effect fiber, asynchronously, outside React’s render pass — and any hook called there would break the Rules of Hooks. So instead, effract drives the generator synchronously, during render, and answers each yield* based on what it is:

You writeeffract does
yield* SomeServiceresolves it synchronously from the runtime’s Context
yield* hook(useState(0))the hook already ran inline — keeps its place in React’s hook order
yield* someAsyncEffectsuspends through React’s use, resumes inline on the retry
a genuine failurethrows to the nearest React error boundary

Because the walk is synchronous and deterministic, your hooks stay valid React hooks with a stable order across renders. effract cooperates with React’s reconciler; it never replaces it.

Why hook(...)

hook(value) lifts an already-evaluated React hook result into the yield* channel, so a component body reads as one uniform stream of yield* whether the value comes from Effect or from React:

const [tab, setTab] = yield* hook(useState('overview'));
const ref = yield* hook(useRef<HTMLDivElement>(null));

The hook is called inlinehook only makes the result yieldable. Since the body runs inside the render pass, the call obeys the Rules of Hooks like any other.

Tradeoffs

effract inherits React’s constraints — a body must yield hooks in a stable order — and adds one of its own: raw yield* asyncEffect caches per component instance by encounter order (load-once semantics). For reactive or refetching data, reach for signals or a service that manages its own caching.

The full design is written up as an architecture decision record: ADR 0001 — Fiber reconciliation.