<!--
Sitemap:
- [rxfy](/index): Typed, normalized, reactive state — built on RxJS
- [Comparison](/comparison): rxfy versus Redux Toolkit, MobX, Jotai, TanStack Query, and TanStack DB
- [Inspired by](/inspired-by): the libraries and ideas rxfy grew out of
- [Agent Skills](/agent-skills): Accurate rxfy context for AI coding assistants
- [Examples](/examples): Runnable apps, from client-only to fully synced
- [Changelog](/changelog)
- [Getting Started](/getting-started)
- [Create Store](/getting-started/create-store): Normalized reactive state in a client-only app
- [Add SSR](/getting-started/add-ssr): Render the first paint on the server, hydrate with no refetch
- [Add Sync Client](/getting-started/add-sync-client): The full stack: the server publishes, the client syncs
- [Core Concepts](/core-concepts): The ideas rxfy is built on
- [Observables](/core-concepts/observables): A value that changes over time, that you can subscribe to
- [Normalization](/core-concepts/normalization): Store each entity once, reference it by id
- [Late Unwrapping](/core-concepts/late-unwrapping): Unwrap async state at the leaf, not the trunk
- [Server-Side Rendering](/core-concepts/ssr): Dehydrate on the server, hydrate with no refetch
- [rxfy](/rxfy): The core package: atoms, lenses, models, and states
- [createModel](/rxfy/create-model): Typed entities in normalized storage
- [defineState](/rxfy/define-state): Typed, normalized state shapes with fetching and mutations
- [createAtom](/rxfy/create-atom): A reactive cell with synchronous get, set, and modify
- [createLens](/rxfy/create-lens): A two-way view into part of an Atom
- [React Bindings](/react): Hooks and helpers for using rxfy in React
- [useStateData](/react/use-state-data): Fetch, normalize, and subscribe to a query
- [useStatePagedData](/react/use-state-paged-data): Paginated and infinite-scroll lists
- [useModelStore](/react/use-model-store): Subscribe to one normalized entity by id
- [useAtom](/react/use-atom): Two-way binding for any IAtom
- [Pending](/react/pending): Render pending, rejected, and fulfilled UI for any observable
- [usePending](/react/use-pending): The status value behind Pending
- [useObservable](/react/use-observable): Bind a raw Observable to React
- [Sync Client in React](/react/sync-client): StoreProvider, useSyncClient, and update handles
- [rxfy-client](/framework/client): The framework-agnostic browser sync runtime
- [createSyncClient](/framework/client/create-sync-client): Connect a transport and drive the sync loop
- [readSsrGrants](/framework/client/read-ssr-grants): Lift SSR-embedded channel grants
- [rxfy-server](/framework/server): Bind Drizzle tables, write, and publish sync updates
- [defineResource](/framework/server/define-resource): Tie a Drizzle table to an rxfy model
- [createSync](/framework/server/create-server): Wire a storage adapter, hub, and secret into a Live object
- [createInMemoryHub](/framework/server/hub): The socket-keyed pub/sub backbone
- [Writes](/framework/server/writes): sync.create / sync.update / sync.delete and touch
- [Storage adapters](/framework/server/storage-adapters): Persist writes with Drizzle or in memory
- [Sync messages](/framework/server/messages): What travels between server and client
- [Grants](/framework/server/grants): The server signs what it serves; the client subscribes with the token
- [rxfy-ws](/framework/ws): The default WebSocket transport
- [createWsServer](/framework/ws/server): Attach a Hub to WebSocket connections
- [createWsClient](/framework/ws/client): The browser transport with reconnect and replay
- [Custom transports](/framework/ws/custom-transport): Bring your own ClientTransport
- [Guides](/guides): Task-focused walkthroughs of common rxfy patterns
- [Pagination and infinite scroll](/guides/pagination): Load and append pages into one normalized list
-->

# Comparison \[rxfy versus Redux Toolkit, MobX, Jotai, TanStack Query, and TanStack DB]

Choosing a state library is mostly choosing where your entities live and what keeps them in
agreement when one of them changes. rxfy answers with a normalized store per model, states that
hold only ids, and one write path that reaches every subscriber — the same declaration carrying the
client, the server render, and the live updates that follow.

The libraries here answer it from different starting points. Redux Toolkit, MobX, and Jotai start
from a client store and reach outward to the server; TanStack Query starts from the fetch and its
cache; TanStack DB starts from a normalized client store with a query engine over it. Each is taken
in its modern, idiomatic form — **Redux Toolkit** (with RTK Query), **MobX 6**, **Jotai**,
**TanStack Query** (React Query v5), and **TanStack DB** — described on its own terms first, then
set against rxfy, then closed with a line on which one to reach for. Where rxfy comes off worse,
the section says so.

## At a glance

✅ built in · ➖ possible, but you wire it up · ❌ not addressed

|                          | rxfy                                                   | Redux Toolkit                            | MobX 6                                 | Jotai                          | TanStack Query                         | TanStack DB                            |
| ------------------------ | ------------------------------------------------------ | ---------------------------------------- | -------------------------------------- | ------------------------------ | -------------------------------------- | -------------------------------------- |
| **Model**                | Reactive states over normalized entities               | Immutable store + reducers               | Mutable observables                    | Composable atoms               | Async cache keyed by query key         | Normalized collections + live queries  |
| **Reactivity**           | Only the changed entity's subscribers                  | Components whose selector output changed | Components that read the changed field | The changed atom's subscribers | Components subscribed to the query key | Live queries, incrementally maintained |
| **Data fetching**        | ✅                                                     | ➖ RTK Query                             | ❌                                     | ➖ async atoms                 | ✅                                     | ✅ collection creators                 |
| **Normalization**        | ✅                                                     | ➖ createEntityAdapter                   | ❌                                     | ➖ manual                      | ➖ manual                              | ✅                                     |
| **Client-side querying** | ➖ state query params                                  | ➖ selectors                             | ➖ computeds                           | ➖ derived atoms               | ➖ select                              | ✅ client-side querying                |
| **SSR**                  | ✅ hydration & streaming                               | ➖ manual                                | ➖ enableStaticRendering               | ➖ helpers                     | ✅ hydration                           | ➖ seed eager collections              |
| **Real-time**            | ✅ via [Sync Client](/getting-started/add-sync-client) | ➖ manual                                | ❌                                     | ➖ manual                      | ➖ manual                              | ✅ via sync engine                     |
| **Validation**           | ✅ Zod                                                 | ➖ RTK Query schemas                     | ❌                                     | ❌                             | ❌                                     | ✅ Standard Schema                     |
| **Boilerplate**          | Low                                                    | Moderate                                 | Very low                               | Very low                       | Low                                    | Moderate                               |

**Client-side querying** means asking a question of data the browser already holds — filter it, sort
it, join it — with no round trip. TanStack DB ships a real query engine for that. rxfy doesn't: you
change a state's params and the server answers again. What you get on the client is *derivation*,
not querying — enough to shape what a component renders, not enough for joins or aggregates. That
job happens on the server instead, where it is typically a query against the database.
[vs TanStack DB](#vs-tanstack-db) covers what that trade buys and what it costs.

## vs Redux Toolkit

Redux centralizes state in one immutable store updated by reducers; components read through
`useSelector`, and RTK Query layers on caching and fetching. It is mature, framework-agnostic, and
those explicit reducers are what make every change auditable through excellent time-travel devtools.
That inspectability is what the boilerplate buys: a slice, RTK Query, and SSR wiring are yours to
compose.

rxfy shares Redux's normalized, immutable-value approach — and its renderer independence: the
`rxfy` core peers only on `rxjs`, `zod`, and `lodash`, so models, states, and stores run in plain
TypeScript, in Node, and in tests with no renderer. Where it differs is that rxfy is
**stream-first** rather than selector-first: each entity is its own `Observable` cell, so a write
notifies only that entity's subscribers instead of re-running every selector. And where Redux
composes a slice, RTK Query, and manual SSR wiring, rxfy folds fetching + normalization + SSR into
`useStateData`:

```tsx
// rxfy: define the shape once, fetch + normalize + cache + SSR come with it
const todosState = defineState({
  key: "todos",
  params: z.object({ filter: z.enum(["all", "active", "done"]) }),
  model: { todos: array(TodoModel) },
  mutations: {
    addTodo: (prev, todo: { id: string; title: string; done: boolean }) => ({
      ...prev,
      todos: [...prev.todos, todo],
    }),
  },
});

const { data$, mutations } = useStateData({ state: todosState, fetchFn: fetchTodos, params });
```

What you give up for that shorter path is Redux's plugin ecosystem and its devtools.

**Reach for Redux Toolkit when** you want the largest ecosystem, time-travel devtools, or your
state is mostly synchronous client state. **Reach for rxfy when** normalized server data,
per-entity reactive rendering, and SSR are the core problem.

## vs MobX

MobX optimizes for the client. It assumes the state is already in the browser: plain objects made
observable and mutated directly, a graph tracked through proxies, `observer` components re-rendering
on the fields they read. That is the shortest path from a write to a re-render, and the
least-ceremony option here for local and UI state. Observability stays out of the types: an
observable field looks like its plain value, so a destructure or a forgotten `observer` leaves you
holding a value that never updates, and nothing catches that at build time — `configure()` dev
warnings and `eslint-plugin-mobx` cover it at runtime instead. Fetching, normalization, SSR, and
real-time all sit outside it.

rxfy trades that mutable transparency for **explicit streams and immutable values**, starting with
the types: an [`Atom<T>`](/rxfy/create-atom) is an RxJS `Observable<T>` and not a `T`, so reading a
value means `get()` or a subscription, and a missed subscription fails the build instead of shipping
a component that stops updating. SSR is where the immutable half pays: entities are plain data keyed
by id, so [`dehydrate`](/core-concepts/ssr) walks the stores and the client puts the same values
back under the same keys. MobX serializes fine via `toJS()`, but its SSR support stops at
`enableStaticRendering` — what to snapshot and how to rebuild it stay yours. Two-way binding splits
the same way: MobX mutates the observable, while rxfy composes a [`Lens`](/rxfy/create-lens) over
the entity's cell in the [model store](/rxfy/create-model#modelstore-get) — one `Atom` per id — and
drives it with [`useAtom`](/react/use-atom), so the write reaches every subscriber. That same hook
drives a standalone `createAtom`, so form fields and toggles don't need a second state library
alongside rxfy.

All of that is rxfy optimizing for the full stack: one declaration covers the fetch, the server
render, the hydration, and the live updates that follow. In an app that never leaves the browser,
none of it pays off and MobX asks for less ceremony.

**Reach for MobX when** you want minimal boilerplate for local/UI/client state and find
direct mutation intuitive. **Reach for rxfy when** you need normalized shared server data,
reactive states, and first-class SSR.

## vs Jotai

Jotai builds state from the bottom up out of **atoms**, tiny composable units that
components subscribe to via `useAtom`. It is minimal, unopinionated, and excellent for local
and shared client state without a central store. An atom carries no opinion about content, which is
what lets it fit any state you invent.

The shared word "atom" hides the difference. A Jotai atom is a **config object** with no value
of its own, resolved against a store — the implicit default one, or a `Provider`-scoped store. An rxfy
[`Atom`](/rxfy/create-atom) is a real **RxJS `Observable` cell** that carries its value,
exposes synchronous `get()`/`set()`, and composes with the full operator set outside React.
Narrowing to a field splits the same way: in Jotai you write a read/write derived atom, or add
`jotai-optics` for `focusAtom` on a nested path, while rxfy ships [`Lens`](/rxfy/create-lens) in the
core — a view + edit optic that is itself an `IAtom`, so a focused field stays an atom that rxfy's
[`useAtom`](/react/use-atom) drives like any other.

Scope differs too. Jotai keeps server concerns reachable rather than provided: async atoms and
hydration helpers exist, while normalization, validation, and real-time are yours to assemble, often
with `jotai-tanstack-query` alongside. rxfy is narrower and batteries-included for one job:
`defineState` + `createModel` give you fetching, normalization, Zod validation, and streaming SSR as
one API. That declaration is worth little for local state and a great deal for normalized data
shared across views.

**Reach for Jotai when** you want a minimal, flexible atomic primitive for local and shared
client state and prefer to compose data fetching and persistence yourself. **Reach for rxfy
when** normalized server data, per-entity reactive rendering, and first-class SSR are the core
problem.

## vs TanStack Query

The split is **document cache vs normalized cells**. React Query keys the *response*, so the
same entity returned by two queries lives in two cache entries; a mutation typically
**invalidates** the affected keys and triggers a refetch to reconcile them. That is the price of
the schema-free cache: one copy per key, plus invalidation to keep the copies agreeing. rxfy asks
for a schema up front instead. You declare models and states, and it normalizes the response into
one cell per entity, so an entity edit — a mutation or a sync push — writes the entity once and
reaches every subscriber **without invalidating or refetching** — and there is no second copy to
drift out of sync. rxfy also validates the response with Zod and turns each
entity into a composable [`Atom`](/rxfy/create-atom) you can drive with
[`useAtom`](/react/use-atom), where React Query hands you a plain snapshot and leaves
persistence and two-way binding to you. Paged and infinite lists get a dedicated hook on both
sides: `useInfiniteQuery` there, [`useStatePagedData`](/react/use-state-paged-data) here, which
keeps each page as a window of ids over the same shared entity stores.

Real-time sharpens the split. React Query leaves the socket to you: subscribe, then call
`setQueryData` or `invalidateQueries` for each affected key, and decide yourself who may
listen to what. rxfy's [sync stack](/getting-started/add-sync-client) ships that pipeline — server
write functions persist through a storage adapter and publish on their own, and the server signs a
channel grant per served state, delivered in the payload, so the client subscribes with exactly that
capability: no allow-list to manage. The store's `added$` signal is what makes that possible — it emits each
entity key the first time it lands and replays for late subscribers, so the sync layer knows
exactly what the client holds without any query wiring its ids in by hand. Edits patch the entity
cell in place; creates and deletes surface as an
"updates available" count ([`updatesAvailable$` / `applyUpdates`](/react/sync-client)), so
the client reloads on its own terms instead of through an eager key invalidation. SSR carries
this through: `sync.hydration` embeds the grants the render already signed next to the dehydrated
data, so a server-rendered page subscribes and resumes receiving pushes along with its cache.

**Reach for TanStack Query when** you want a mature, framework-agnostic cache with rich
refetch/staleness controls and don't need normalization. **Reach for rxfy when** the same
entity appears across many queries and you want one update to reach every subscriber without a
refetch, plus built-in normalization, validation, and streaming SSR.

## vs TanStack DB

TanStack DB sits closest to rxfy of anything here. Both keep a normalized client
store keyed by entity id, and both notify only the subscribers of the entity that changed. You
load records into DB collections, fed by a TanStack Query fetch, a sync engine like Electric or
PowerSync, or a collection factory you write yourself. Components read those collections through
live queries built from operators like `from`, `where`, `join`, and `select`. When new data arrives,
DB updates only the rows of the result it affects instead of re-running the query. Writes land
locally first, persist through the collection's `onInsert`, `onUpdate`, and `onDelete` handlers, and
roll back when the server rejects them.

DB and rxfy run the query in different places, and that shapes your API. DB puts the query engine
in the browser, so components query collections it has already loaded: another screen is another
live query, and your backend stays unchanged. You pick how much each collection holds: eager mode
pulls the whole thing, on-demand mode pushes your query's predicates down to the collection's
loader — which for a query collection means parameters on your own endpoint — and progressive mode
combines the two, serving the first queried subset on demand while the full set syncs in behind it.
rxfy keeps the query on the server.
[`defineState`](/rxfy/define-state) declares what a page needs and your fetch function answers it
with full entities, which rxfy normalizes on arrival: rows land in per-model stores keyed by id, and
the state itself keeps only those ids as references to normalized entities. Filtering, sorting,
authorization, and [pagination](/guides/pagination) stay your server's job — rxfy ships the client
half of the last one as [`useStatePagedData`](/react/use-state-paged-data), which windows ids over
the shared stores — so a new screen usually means another state and an endpoint serving its exact
shape, which is the cost of keeping the server as the query authority.

For real-time, DB ships no server of its own, which is what lets it sit in front of any backend.
That leaves you three ways in: adopt a sync engine and let it stream changes for you — Electric,
PowerSync, TrailBase, and RxDB all have official collections; implement DB's own sync interface
(`begin` / `write` / `commit` / `markReady`), the documented extension point every one of those
adapters is built on, which the docs work through with a WebSocket example; or push individual
messages into a collection through its write utils. All three still leave the socket, its auth, and
its fan-out to you. rxfy ships both
halves: a resource binds a table you already own to the model the client renders. Writes leave through a
[storage adapter](/framework/server/storage-adapters), a port of three methods, so the persistence
layer stays replaceable.

SSR is the key distinction and the case rxfy was built around. Every state
[dehydrates](/core-concepts/ssr) into the HTML and hydrates on the client with no refetch and no
loading flash, buffered or streamed, so a page arrives rendered and already receiving live updates.
With DB, the server can only seed collections and never render from them: DB snapshots collection
state for its own local persistence layer, never into the server render, and ships no
server-rendering guide — SSR is explicitly still on its path to v1. Seeding is what's left: an
eager collection takes `initialData` for rows your server already rendered, while on-demand
collections leave you to prime the Query cache entry by hand. A progressive collection shortens the
wait for its first rows, but the wait still starts after the bundle boots — those rows are a client
fetch, not markup that arrived rendered.

**Reach for TanStack DB when** you want a relational query engine in the browser: joins and
aggregates over local collections, optimistic writes with rollback, and your choice of sync engine
behind them. **Reach for rxfy when** the server stays the query authority and you want normalized
reactive entities, real-time without building the push layer, and first-class SSR.

## vs sync engines

**Convex**, **Supabase Realtime**, **Electric SQL**, and **Zero**/**Replicache** also stream server
changes into client state. The difference is what you adopt. A sync engine hands you the whole
platform in one decision — offline writes, conflict resolution, managed infrastructure — and that
decision reaches all the way down to the database: your schema, queries, and often your hosting move
onto its platform or its replication protocol. rxfy binds to what you already run instead: a
resource ties a table you own to the same model the client renders, and writes stay in your own API
routes behind a swappable
[storage adapter](/framework/server/storage-adapters). You also choose how far up to go — a
client-only [store](/getting-started/create-store), then [SSR](/getting-started/add-ssr), then the
[Sync Client](/getting-started/add-sync-client) — and only that last step brings a server into it.
Offline writes and conflict resolution stay yours.

**Reach for a sync engine when** you want the whole platform: offline writes, conflict
resolution, managed infrastructure. **Reach for rxfy when** you have a database-plus-fetch
app and want real-time sync without re-platforming it.

## When rxfy is a good fit

* Your app is driven by **normalized server data** that several views render at once.
* A single entity update — a refetch, a mutation, a push from another client — must reach every
  subscriber **without re-fetching or re-selecting**, and when it has to **appear across clients as
  it happens** you'd rather not build the push layer, its auth, and its transport yourself: the
  [Sync Client](/getting-started/add-sync-client) level ships them.
* Your app needs **SSR** that comes built in rather than assembled: pages arrive rendered, so they
  paint sooner and search engines can index them.
* You're not put off by what **RxJS** costs. It asks you to learn a new mental model and to think in
  data streams, and in return it unlocks a whole different way of how you work with data.

None of that makes rxfy the right answer everywhere. An app that is only local UI state gets nothing
from normalization, SSR, or sync, and MobX or Jotai ask for less. A team that wants a large plugin
ecosystem or time-travel debugging, or that won't take on RxJS, is better served by Redux Toolkit.
A client that must answer its own questions with no round trip — joins, aggregates, ad-hoc views
over a working set it already holds — wants TanStack DB's query engine.

What rxfy covers is the span none of the others cover end to end: one declaration that fetches,
validates, and normalizes; those same entities rendered on the server and rehydrated with no second
fetch and no flash; and live updates out of box. Each library here does part of that well, and each leaves you to assemble
the rest. rxfy is built to treat it as one job.
