Skip to content

vanya2h/rxfy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

861 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rxfy

rxfy (/ɑɹ ɪks faɪ/) is a reactive data-flow layer for your React app: declare typed models, states, and normalized stores, and scale from a client-only store to a fully live app with server-side rendering and real-time updates via websockets. It's built for consistency and granular RxJS-based reactivity at no extra cost.

The problem

Render one piece of data in two components and keep them in agreement. Now render it in six — a list row, a detail panel, a sidebar counter, a search result — with two people editing it at once, and the job grows into a large share of what your app does. Much of the code you write around fetching exists only to keep those copies in sync.

Tools like TanStack Query handle this by invalidation: after a mutation you invalidate the affected query keys and refetch, and you have to get every key right, every time. Both the complexity and the risk of a missed invalidation grow non-linearly with the number of places the entity appears.

Problem deconstruction

The root cause is one thing: your app holds many copies of the same entity. The list has one, the detail view fetched another, the cache may hold a third, and every write has to find and update all of them. Add a second client and the copies span the network too.

Your database does not have this problem. Each row is one piece of data, kept in one place — a single source of truth. The question is how to carry that property up to the client, in a framework that:

  • keeps maintenance cost flat as the number of views grows
  • lets you declare each data model once and share it between server and client
  • routes every write to one place, and from there to every view that renders the entity, with no manual invalidation
  • re-renders only the components whose data actually changed
  • serializes state on the server and restores it on the client, so SSR reuses the same models
  • takes real-time updates over a websocket through that same single path
  • composes and transforms the data as streams, so each view can derive exactly the shape it needs

Solution overview

rxfy stores each entity once, in a normalized store keyed by its id, and every view references that slot instead of holding a copy. One write reaches every subscriber, and the same declaration carries the client, the server, and the wire.

Client-side

Declare an entity model and store each entity in its own slot of the model store, addressed by its id. Compose a state from those models — each page has its own — and derive it as an observable, using RxJS operators to shape it for any view. Live updates flow into the same store: a patch updates the entity's slot and every subscriber re-renders, while a create or delete marks every state that lists it stale, ready to sync when you choose.

Server-side

Bind each model to its database table with a resource, so the server reads and writes the same model the client renders. On a request it fills the page's state and serializes it into the HTML; the client restores that snapshot on hydration and skips the fetch. Writes go through the resource — an update publishes a patch on the entity's topic — and the server signs a grant per served state, so each client subscribes to exactly the entities it was handed and no others.

sequenceDiagram
    participant A as Alice
    participant S as Server
    participant DB as Database
    participant B as Bob

    A->>A: update entity in the store
    A->>S: send mutation
    S->>DB: persist change
    S-->>A: publish updated entity
    S-->>B: publish updated entity
    Note over A: store recalculates every place<br/>this entity is used
    Note over B: store recalculates every place<br/>this entity is used
Loading

Core principles

rxfy is built on five principles:

  • Everything is a data stream. rxfy builds on RxJS, not a reactivity system of its own: an Atom is an Observable with synchronous get() and set(), and entity cells, state data$, and lenses are all streams. The operator library composes over your app state, a derived value recomputes itself when its source changes, and a websocket push is just another stream flowing into the same cells.
  • Data normalization. Each entity lives once, in a single slot keyed by its id, and every view references that slot instead of holding its own copy — no second copy to drift, and a change has exactly one place to happen.
  • Late unwrapping: data travels wrapped, only the leaf component that renders a value unwraps it, and a write never unwraps anything — see Late Unwrapping.
  • No model copies. Each model shape and each state is declared once and used on both sides; the server fills and serializes a state, the client restores it and skips the fetch — SSR with no DTO layer, no client-only type, no separate path.
  • Static validation. Every model, state, param, and mutation is typed, so a wrong shape is a compile error; the store is keyed by a branded StoreKey, and because each model is a Zod schema, one declaration validates the data at runtime too.

Alternative approaches

None of these principles are new on their own — normalized stores, observable state, SSR, and real-time sync all exist elsewhere. rxfy holds all of them behind one declaration. TanStack Query keys its cache by response and leaves invalidation to you; Redux Toolkit normalizes but notifies through selectors rather than streams; sync engines stream normalized changes too, but ask you to move your schema and hosting onto their platform, where rxfy layers onto the database and API routes you already run. See the comparison for where each one fits.

📚 Documentation: rxfy.vanya2h.me

Agent skills

# getting rxfy into a project (template or add-to-existing-app)
npx skills add vanya2h/rxfy --skill rxfy-setup

# working in a project that already has rxfy
npx skills add vanya2h/rxfy --skill rxfy

Two agent skills for AI coding assistants: rxfy-setup (scaffold a create-rxfy-app template or add rxfy to an existing app at a chosen depth) and rxfy (a task-indexed reference library for the whole framework — store, React, SSR, real-time sync). Setup records the chosen variant so usage never re-detects the project type. See Agent Skills.

Packages

Package Purpose
rxfy Core library: Atom, Lens, Wrapped, Models/States API, SSR dehydrate/hydrate
rxfy-react Official React bindings (rxfy-react/next for Next.js App Router)
rxfy-server Storage-agnostic sync server: write + publish, signed grants
rxfy-server-drizzle Drizzle/Postgres storage adapter (defineResource, drizzleStorage)
rxfy-server-memory In-memory storage adapter (defineCollection, memoryStorage)
rxfy-client Framework-agnostic browser sync runtime: grant custody, renewal, replay
rxfy-protocol Wire protocol and codec for sync updates
rxfy-ws Default WebSocket transport (client + server)

Install

npm install rxfy rxfy-react
# peer deps: rxjs zod lodash react react-dom @types/react

Links

Documentation

Core Concepts

API Reference

Guides

Examples

  • vite-blog — live blog: SSR + WebSocket patches/stale, HMAC grants (Vite · Hono · PGlite · Drizzle · rxfy-server · rxfy-ws)
  • vite-ssr-pagination — infinite paginated list with a switch between Load-more button and infinite scroll; streaming SSR; rows generated on demand with faker
  • next-blog — Next.js App Router with streaming SSR
  • rr7-blog — React Router 7 (framework mode) with buffered SSR; rxfy as the single data layer, loaders for routing only
  • waku-blog — Waku (minimal RSC framework); static home + dynamic post, server-component prefetch + prop hydration (no injection seam)

License

MIT

Releases

Packages

Used by

Contributors

Languages