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+ · Effect v4 · ~5 KB
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.
The core is ~5 KB gzip. React and Effect stay peers (your app’s copy) — never bundled or shipped twice, even across minor versions.
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.