Affluente
affluente (Italian): a tributary, a small stream flowing into a bigger one.
A component is just an Observable<HTMLElement>. That's the whole framework.
A user interface is a pile of values that change over time. Affluente takes that literally: an element
is reactive because it is a stream. There's no virtual DOM, nothing to diff, no re-render cycle - a
single mount at the root (addToView) sets the whole app in motion, and state changes hit the DOM
immediately. No useState, no dependency arrays, no memoization, and none of the bugs that come with a
render cycle, because there isn't one.
The catch with reactive streams has always been that they're intimidating to write. So the optional Reactive TS layer lets you write the plain expression and lifts it into the exact reactive stream for you, fully typed. Here's the whole idea in one line - take the cursor's position and derive a colour from it with ordinary maths:
const hue = (mouseMovementX / 4) % 360; // plain maths, but hue is live
Push the mouse position into mouseMovementX, drop hue into a style, and the gradient tracks your cursor:
import { Div, State } from "affluente";
export const ReactiveGradient = () => {
const mouseMovementX = new State(0);
const hue = mouseMovementX % 360; // a number stream, derived with ordinary maths
return Div.onMousemove((e) => mouseMovementX.next(e.offsetX)).style({
background: `linear-gradient(135deg, hsl(${hue} 85% 55%), hsl(${hue + 60} 85% 60%))`,
})`Move your mouse`;
};
Move your mouse across it: no CSS animation could follow the cursor like this. hue is live because
mouseMovementX is, so the moment it lands in the style string the element is bound to it and repaints as you
move. There's no render cycle scheduling it, because there isn't one anywhere in Affluente.
The same trick works over time. frames(), timer(due, period), and interval(period) are reactive
clocks - derive a value from one and it updates on its own, with no loop to write. Pass a stream for the
rate to change it on the fly, or null to stop.
State works the same way. Here a click count drives a derived value, written as a plain expression that stays reactive:
import { Button, State, addToView } from "affluente";
const ClickCounter = () => {
const clicks = new State(0);
const doubled = clicks * 2; // count * 2 stays reactive, no map and no pipe
return Button.onClick(() =>
clicks.update((c) => c + 1),
)`Clicks: ${clicks} (doubled: ${doubled})`;
};
addToView(ClickCounter()); // the one subscription your app needs - mounts to document.body
That clicks * 2 is the whole pitch. Reactive TS lifts it to the exact RxJS that runs underneath:
const doubled = clicks * 2; // with Reactive TS
const doubled = clicks.pipe(map((c) => c * 2)); // the RxJS it runs as
No new runtime model, nothing hidden, just less ceremony.
Components are functions for a reason: a component's State is declared inside it, so each call -
ClickCounter() - is an independent instance with its own state. Write components as functions and
instantiate them where you mount them; a single built component value (state declared at module scope,
or ClickCounter() called once and reused) shares that state across every place it appears.
Fluent operators, straight from the proposal
State (used above) is a writable value: read it with .value, set it with .next(...), or
.update((c) => c + 1) when the next value comes from the current one. (Under the hood it's an RxJS
BehaviorSubject.) Every stream also carries fluent operator methods, modelled on the
WICG Observable proposal, so chaining reads the way the platform
itself is heading:
import { State } from "affluente";
const query = new State("");
const settled = query.debounce(200); // wait for a 200ms pause, then emit the latest value
The set follows the proposal (map, filter, take, drop, takeUntil, catch, finally,
flatMap), plus scan (a running fold) and debounce / throttle. They're real methods on the
stream, so the same code is one polyfill away from running on a native Observable if the proposal
ships, and in a .rts file they're offered in autocomplete and lift as genuine stream operators.
Why Affluente
- Streams are the components. No virtual DOM, no reconciliation, no render scheduling - state changes hit the DOM immediately.
- It's just RxJS. Everything composes with the operators and patterns you already know;
rxjsis the only dependency. - Tiny and transparent. A small operator library over
Observable- easy to read, easy to reason about what the framework is doing. - Plain expressions, fully typed (Reactive TS). Write
count * 2,a === b,cond ? x : y; your editor shows real inferred types live, with noanyand no false errors.
Quick start
npm install affluente rxjs@^7
…then see Getting started. To run the example app locally:
git clone https://github.com/alden12/rxfm && cd rxfm
yarn && yarn dev # http://localhost:3000
Or browse the live demo.
Documentation
| Getting started | Install, editor setup, and the Reactive TS build. |
| Guide | The full walkthrough - components, state, attributes, lists. |
| Examples | The Reactive TS example suite that powers the live demo. |
| Plain-TypeScript reference | Affluente in plain RxJS, no build step. |
| Reactive TS roadmap | Status of the experimental Reactive TS layer. |
Alpha. This is the
3.0.0-alphaline - an in-progress redesign (Vite build, no JSX, a new fluent component API). The API may change between alpha versions. For the current stable release and its JSX/TSX syntax, see the v2.1.1 README.Reactive TS is experimental. The transform, Vite plugin, and editor extension are a spike that currently lives in this repo (not yet on npm) - see the roadmap. Plain Affluente needs none of it.
I'd love to hear whether this style holds any interest for you - feedback and ideas are very welcome. These docs will eventually move to the project site (github.io), which currently hosts the demo.
Built on RxJS. MIT licensed. Authored by Alden Laslett.