---
title: effract vs the alternatives
description: Honest comparisons — effract vs effect-atom, vs TanStack Query + Zustand, and vs the forked-renderer generator libraries (Crank.js, effect-ui).
group: Start
order: 5
---

"vs what?" is the fair first question. effract is a **component model**: you write a React component as
an Effect program, and an interpreter drives its generator body **inside React's render pass** — no
forked reconciler. Below is how that lands against the tools you likely reach for today. Each section
includes the rows where the other side wins; picking well means knowing both.

## vs effect-atom

effect-atom and effract share the Effect ecosystem, and it shows — if you already think in `Atom`s, a
lot here will feel familiar. The difference is scope. **effect-atom binds state into a component through
hooks** (`useAtomValue`, `useAtomSet`): the component stays an ordinary React function that reaches out
to atoms. **effract makes the whole component an Effect program** — the body is a generator, and every
`yield*` (a service, a `query`, a signal, a hook) is one uniform channel:

```tsx
import { rec, query } from '@tmonier/effract';

const Profile = rec(function* () {
  const user = yield* query(fetchUser(id), id); // async data — suspends, refetches, cancels
  return <Card user={user} />;
}).catch({ NotFound: () => <Empty /> }); // exhaustive over the error channel
```

Because the body *is* the program, effract can turn three things the type system usually can't see into
**compile-checked obligations**:

- **Errors** — `.catch({ Tag: (error) => fallback })` is [exhaustive over the tagged error
  channel](/docs/errors/). Forget a tag the body can fail with and it will not compile.
- **Loading** — a `query`/`suspend` records a [loading obligation](/docs/loading/) that `mount` refuses to
  compile until you discharge it with `.suspense(fallback)` or `mount(layer, Root, { loading })`.
- **Volatility** — a stream atom records a [render obligation](/docs/render-control/) that `mount` refuses
  to compile until you defer the read (`defer(atom)`) or acknowledge the heat (`.hot()`).

And one `mount`, from one package, renders the same body in a SPA, during SSR, or (resolve-up-front) as
an RSC — the boundary is identical on the client and the server.

**Credit where it's due.** effect-atom did prior art that directly informed effract. Its serialization
story (`Atom.serializable`) shaped effract's `atom.serializable(key, initial, schema)` + automatic
hydration, and its optimistic-update model (`Atom.optimistic`) informed effract's
`mutation(label, run, { optimistic, invalidates })`. These are good ideas we learned from, not ones
we're claiming against it.

**Where effect-atom wins today.** It is older, more widely installed, and more battle-tested. If your
priority is a larger install base and the maturity that comes with it, effect-atom is the safer pick
right now — effract is the newer, more opinionated bet.

## vs TanStack Query + Zustand

This is the named 2026 default: TanStack Query for server state, Zustand for client state, and often
React Hook Form + nuqs alongside. It's an excellent stack. The trade is **coverage and runtime rigor**
(the default stack) versus **compile-time guarantees and one primitive** (effract).

| Concern | effract | TanStack Query + Zustand |
| --- | --- | --- |
| Typed errors | **Yes** — compile-checked `.catch`, exhaustive per `_tag` | Runtime — errors are values you remember to branch on |
| Compile-checked loading obligation | **Yes** — `mount` won't compile until discharged | No — a forgotten `isLoading` is a runtime bug |
| Renderer-free testing | **Yes** — services are plain Effect, unit-tested with no DOM | Hook-based — needs a renderer / `renderHook` |
| Mutations | **In services** — `mutation()` next to the data it changes; typed rollback; testable with no DOM | `useMutation` — a component hook; the optimistic policy lives in the component |
| Time-dependent tests | **Deterministic** via Effect's `TestClock` | Real timers or hand-rolled mocks |
| Primitives to learn | **One** — a `yield*` covers state, fetch, and errors | Two libraries, often plus RHF + nuqs |
| One mount for SPA / SSR / RSC | **Yes** — same body, same `mount` | Separate wiring per target |
| Ecosystem maturity | Newer, smaller | **Large, proven, widely adopted** |
| Docs & learning material | Growing | **Extensive** — tutorials, courses, StackOverflow |
| Devtools | Redux DevTools bridge + `<Inspector />` (just shipped) | **Mature, polished** panels |

Two honest caveats on the effract column. The devtools gap was real; effract now ships a
`connectReduxDevtools()` bridge (the Redux DevTools browser extension) and an in-app `<Inspector />`, but
TanStack's devtools are older and more refined. And on maturity and documentation volume the default
stack is simply ahead — that's the cost of being newer. What effract buys in return is that the errors
and loading states you'd otherwise remember to handle become things the compiler won't let you forget —
and that the write side moves out of components: a TanStack mutation is a hook, so its optimistic policy
lives in a component and tests through a renderer; an effract `mutation()` lives in the service that
owns the data, rolls back on a *typed* failure, and tests as a plain Effect program, no DOM.

## Render control vs the React Compiler

The React Compiler memoizes **silently**: it makes many re-renders cheaper, but it cannot tell you why a
component rendered, and it has no opinion about a subscription that renders sixty times a second —
that's still your bug, just a faster one. effract treats render behavior as a **channel**: every atom is
named, every render is attributed to its cause (atom · query · props · hook · parent), and a volatile
subscription is a compile error until acknowledged:

```tsx
mount(App, Ticker); // ✗ effract: volatile subscription not handled
```

```tsx
type _lock = RendersOn<typeof Dashboard>; // 'stats' | 'filter' — lock it in a type test
```

And where the Compiler memoizes for you, silently, effract refuses automagic: `.memo()` is explicit,
and it compiles only when the types prove it sound — every prop value-comparable, or a branded error
naming the offending prop. See [render control](/docs/render-control/).

## vs Crank.js / effect-ui

This is the real differentiator, so it's worth stating plainly. Crank.js and the effect-ui-style
projects share effract's appeal — a **generator component** whose body reads top-to-bottom. But they
resolve the generator-versus-component tension with a **forked renderer**: their own reconciler, not
React's.

A fork is a clean design, and it costs the entire React platform:

- **React DevTools** — gone; the fiber tree the extension inspects isn't there.
- **RSC** — gone; React Server Components are a React-reconciler feature.
- **Hydration** — gone; you re-implement or forgo React's hydration.
- **The React ecosystem** — every library that expects real fibers, contexts, and Suspense.

effract makes the opposite choice: it renders **through** real React. The generator body is driven by an
interpreter **inside React's render pass** — there is no forked reconciler, so `yield*` is just a way to
author what remains a genuine React component. React DevTools sees ordinary fibers, RSC and hydration
work because they are React's own, and the ecosystem keeps working because nothing was replaced. See
[the thesis](/docs/the-thesis/) for how the two fiber schedulers reconcile.

That's the core wedge: **generator ergonomics without leaving React.**

## When effract is not the answer

Comparisons cut both ways. If you need a proven, deeply documented stack with mature devtools *today*,
reach for TanStack Query + Zustand. If you want state binding into otherwise-plain React components
without adopting a component model, effect-atom fits. If you're willing to trade the React platform for a
purpose-built generator renderer, Crank.js is a coherent choice. effract earns its place when you want
compile-checked errors and loading, renderer-free and `TestClock`-deterministic tests, one primitive, and
one `mount` for SPA/SSR/RSC — all while staying 100% real React. For a fuller self-assessment, see
[is effract for you?](/docs/is-effract-for-you/).
