Effect-native React

Write React components as Effect programs.

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

Two fibers, one component

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.tsx
// 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

Server vs client is just a mount(...)

Browser layer → SPA. Server layer → streaming SSR. Flight renderer → RSC. The component never changes; only the runtime under it does.

SPA

Vite, in the browser

SSR

Bun / Node streaming

Web Worker

off the main thread

RSC

Flight, streamed

The same RECs in all four — proven by the example apps.

Philosophy

Keep the rendering layer boring

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.

The hard parts live in Effect

Retries, concurrency, caching, tracing — Effect’s job, outside the render tree, testable and reusable.

Async composition becomes trivial

No loading flags, no cascading hooks. Everything resolves before render; the body reads top-to-bottom.

RSC’s good idea, without the server

RSC’s real win was resolving dependencies at the composition root. effract keeps that, drops the lock-in.

One primitive, not a zoo

Retire useEffect, a fetch library, context, a store, server actions — one yield* covers all three.

Why effract

The call site, above all

01

Incremental, not a rewrite

Plain components stay plain <Component /> JSX. Write a REC only where one needs a service.

02

Real React, all the way down

Hooks, Suspense, error boundaries, hydration, RSC — all just work. effract renders through React.

03

Services, synchronously

Reading a service is a Context lookup, not an async round-trip. No Effect.runSync at the call site.

04

Signals without ceremony

observe($ => $(count) * 2) re-renders exactly when an atom you read changes. No provider, no selectors.

05

RSC, natively

The same body becomes an async Server Component and streams standard Flight.

06

Typed end to end

Requirements and errors are inferred from your yields — mount won’t compile without them.

Write it once. Run it anywhere a runtime does.

MIT, on npm. Start with the docs, or the eight call-site recipes.