Dataflow logo: GitHub

Dataflow is a lightweight reactive JavaScript library heavily inspired by Observable Framework. It brings reactive programming to plain HTML pages with minimal overhead - the core library (including JSX support) is under 3KB compressed!

Just add a single data-reactive attribute to any script tag and it becomes reactive. That's it.

It works like a spreadsheet: just reference a value from another reactive script tag, and the plumbing happens automatically. No setState, no Redux, no reducers, no signals, no manual subscriptions. Just declarative dependencies.

Document order does not matter, all reactive scripts will be topologically sorted and turned into a DAG before execution.

The transformation can happen at build time, at runtime on the server, at the edge, or even client-side using a service worker.

Example

A live clock using a generator that yields the current time:

<script type="module" data-reactive>
`The current time is ${new Date(now).toLocaleTimeString("en-GB")}.`
</script>

Expressions and Display

A single expression automatically displays its result:

<script type="module" data-reactive>
1 + 2
</script>
Note: Unlike Observable, a semicolon on the end of a single expression does not change how it behaves. A single expression is always a singles expression regardless of optional semicolons. Prettier will not break anything.

Statements like variable declarations don't display anything - they just define values for other blocks to use:

<script type="module" data-reactive>
const foo = 1 + 2;
</script>

The variable foo is defined but nothing is displayed.

For explicit control, import the display function. You can call it multiple times:

<script type="module" data-reactive>
for (let i = 0; i < 5; ++i) {
            display(i);
        }
</script>

Imports

Any import in a reactive script is available to all other reactive scripts. So the imported display function is now available (document order does not matter)

<script type="module" data-reactive>
import {display, view, input} from '@bodar/dataflow/runtime.ts';
</script>
Note: Unlike Observable you do need to import display, view, input etc at least once if you want to use them.
<script type="module" data-reactive>
display('Using previously imported display function');
</script>
Note: All the examples from now on will not show the enclosing reactive script tag

DOM Nodes

If an expression evaluates to a DOM node, the node is inserted into the page. Use this to create dynamic content.

document.createTextNode("[insert chart here]")

JSX

JSX is supported for creating DOM elements with a familiar syntax. Under the hood, JSX compiles directly to native DOM methods using the @bodar/jsx2dom library - no virtual DOM, no framework overhead.

For example, here's a component that displays a greeting:

const greeting = (name) => <i>Hello {name}!</i>
greeting("World")

JSX attributes work as expected, including dynamic values:

const color = "blue";
<span style={`color: ${color}; font-weight: bold`}>Styled with JSX</span>

Reactivity with Generators

Code blocks automatically re-run when referenced reactive variables change. The block below references a now variable representing the current time in milliseconds; because now is reactive (defined as a generator), this block runs continuously.

const now = function* () {
            while (true) {
                yield Date.now();
            }
        }
<span
        style={`color: hsl(${(now / 10) % 360} 100% 50%)`}>Rainbow text!</span>

Reactive Inputs with view()

The view function combines display and input to create reactive inputs. When the input changes, dependent code re-runs.

Using Plain JSX Inputs

const name = view(<input name="name" type="text" value="World"/>);
`Hello, ${name}!`

Using Observable Inputs

You can also use the @observablehq/inputs library for more sophisticated input controls.

import {range} from "@observablehq/inputs";

        const slider = view(range([0, 100], {label: "Value", step: 1, value: 50}));
`The slider value is: ${slider}`

Multiple Inputs

const a = view(<input type="number" value="10" style="width: 60px"/>);
const b = view(<input type="number" value="5" style="width: 60px"/>);
<span>{a} + {b} = {a + b}</span>

Different Input Types

Checkbox

const checked = view(<input type="checkbox" id="feature"/>);

Select

const color1 = view(<select>
            <option value="red">Red</option>
            <option value="green">Green</option>
            <option value="blue">Blue</option>
        </select>);
<span style={`color: ${color1}; font-weight: bold`}>Selected color: {color1}</span>

Combining Multiple Reactive Values

Reactive values can depend on multiple inputs. The dependent code re-runs whenever any input changes.

const width = view(<input type="range" min="50" max="300" value="150"/>);
const height = view(<input type="range" min="50" max="200" value="100"/>);
const color2 = view(<input type="color" value="#4a90d9"/>);
<div style={`width: ${width}px; height: ${height}px; background: ${color2};
    display: flex; align-items: center; justify-content: center; color: white;
    text-shadow: 1px 1px 2px black`}>
            {width} x {height}
        </div>

Examples