Incremental, not a rewrite
Plain components stay plain <Component /> JSX. Write a REC only where one needs a service.
The same component runs in a SPA, on a server, in a Web Worker, or as an RSC — server vs client is just a runtime detail.
MIT · React 19.2+ · Effect v4
The thesis
React and Effect both schedule work on fibers. effract drives a component’s generator inside React’s render pass — one stream of yield* for both. 100% real React, no forked reconciler.
yield* Stats a service, resolved synchronously yield* hook(useState(0)) a real React hook, stable order yield* fetchUser suspends, then resumes inline a failure throws to the nearest boundary // Counter: a React component,
// written as an Effect program
const Counter = rec(function* () {
const stats = yield* Stats
const [n] = yield* hook(useState(0))
return <Row n={n} of={stats.total}/>
})
// DI checked at compile time:
createRoot(el).render(
mount(AppLive, Counter),
) A service and a real React hook in one body — checked at compile time.
One component, every runtime
Browser layer → SPA. Server layer → streaming SSR. Flight renderer → RSC. The component never changes; only the runtime under it does.
Vite, in the browser
Bun / Node streaming
off the main thread
Flight, streamed
The same RECs in all four — proven by the example apps.
Philosophy
A React component should be almost dull — structure and interaction, nothing more. The hard parts live outside React, resolved at the composition boundary and handed to your JSX as a typed result.
Retries, concurrency, caching, tracing — Effect’s job, outside the render tree, testable and reusable.
No loading flags, no cascading hooks. Everything resolves before render; the body reads top-to-bottom.
RSC’s real win was resolving dependencies at the composition root. effract keeps that, drops the lock-in.
Retire useEffect, a fetch library, context, a store, server actions — one yield* covers all three.
Why effract
Plain components stay plain <Component /> JSX. Write a REC only where one needs a service.
Hooks, Suspense, error boundaries, hydration, RSC — all just work. effract renders through React.
Reading a service is a Context lookup, not an async round-trip. No Effect.runSync at the call site.
observe($ => $(count) * 2) re-renders exactly when an atom you read changes. No provider, no selectors.
The same body becomes an async Server Component and streams standard Flight.
Requirements and errors are inferred from your yields — mount won’t compile without them.
MIT, on npm. Start with the docs, or the eight call-site recipes.