react-dataflow
A dataflow execution library for React.
It helps you build applications where your components are the business logic; the behaviour of the application arises from the way the components have been connected, like a circuit. This is in stark contrast to conventional React, where well-architected applications usually emphasise a clear separation between presentation and computation elements.
By contrast, react-dataflow
is a "data-first" perspective of React; where we directly utilise the React DOM to drive efficient updates using the powerful Top Level API, meanwhile the ability to render an equivalent frontend comes as a happy biproduct.
Instead of propagating data values directly via props, react-dataflow
enables you to distribute data indirectly using wires. These permit conventional React components to share data independently of scope, and enables deeply-nested updates in self-managing child components to drive changes towards components anywhere in the hierarchy, without explicit handling. This makes react-dataflow
more conducive to describing flow-based computation in React.
🚀 Getting Started
Using npm
:
npm install --save react-dataflow
Using yarn
:
yarn add react-dataflow
✍️ Tutorial
To use react-dataflow
, your top-level application needs to be wrapped with the withDataflow
HOC. This injects all of the required dependencies for dataflow-driven execution into your <App />
:
;; const App = <ReactFragment />; App;
Nothing special, right?
Well, dataflow isn't very useful without having sources of data, so let's create one! A great example of a data source is a clock signal, like the ones we find in digital logic circuits. These emit a bistable signal which oscillates between a high and low voltage at a fixed interval, and are useful for enforcing synchronization between distributed tasks.
First, let's see how we'd create one of these signals using conventional React:
;; const DigitalClock = React;
Here, we use the useRaf
hook to repeatedly request smooth animated render frames for our component. On each frame, the component is re-rendered, and we continuously invert the contents of our signalBuffer
, which is returned as a child. This generates the output true
, false
, true
, false
over and over again.
This helps establish the square wave "form" of data we're interested in using pure React, but let's turn our attention over to some of the restrictions.
Firstly, this is a pretty boring application! All we do is render a constantly flickering boolean string on the DOM when we render a <DigitalClock />
. Of course, we could dress this up, but there's a much bigger problem at play; what happens when we want actually want to use the output value to drive a change somewhere else in our DOM? In traditional React, we would have to nest some child components which should be sensitive to these changes, but then our DigitalClock
starts to be responsible for a lot more; it stops being just a DigitalClock
, and more like a DigitalClockProvider
.
Alternatively, we could use a callback function which manipulates the state of our parent, but this causes the parent component to re-render and requires the parent to manage propagation of the signal itself, when it doesn't necessarily require an informed interest in the value of the signal. By contrast, when using dataflow, it's trivial to re-route data between consumers, and allow passed messages to execute asynchronously, independent of the parent state.
To demonstrate these shortcomings, imagine that we wish to connect our <DigitalClock />
compnent to a separate <LightEmittingDiode />
component, which will light up when the clock becomes active
:
const LightEmittingDiode = <div style= // XXX: Leave this part to your imagination! style backgroundColor: active ? 'green' : 'grey' />;
How would it be possible to connect the LightEmittingDiode's input active
prop to the output of the DigitalClock
? Well... we could use a wire:
;;; // XXX: Here, data is passed along a wire!const App = { const wire = ; return <> <DigitalClock cout=wire /> <LightEmittingDiode active=wire /> </> ;}; App;
Here, the DigitalClock
's cout
prop is connected to the wire we've created by making a call to the useWire
hook. Conversely, the LightEmittingDiode
's active
prop has also been connected to the same wire. Meaning, that whenever the DigitalClock
's cout
prop is changed, our LightEmittingDiode
is automatically re-rendered using the new value that is sourced by the wire
.
An additional benefit to this is that because a wire
reference itself is effectively a constant, our top-level <App />
instance is only rendered once, even though our DigitalClock
and LightEmittingDiode
are constantly re-rendering with each cycle.
Complete Example
In this example, we render a DigitalClock
, an Inverter
and a LightEmittingDiode
. Here, whenever the clk
signal goes high, the LightEmittingDiode
will become inactive, and vice-versa. This allows our LightEmittingDiode
to behave like an "active low" component.
Notice that in order to render an <Export />
component to manage the propagation of your component output props along a connected wire, you are required to specify an exportPropTypes
attribute. This enables react-dataflow
to efficiently manage, and validate, signal propagation using wires:
;;; ; const Clock = React; ClockexportPropTypes = cout: PropTypesbool;const Inverter = <Export output=!input />; InverterexportPropTypes = output: PropTypesbool; const LightEmittingDiode = <div style= width: 100 height: 100 backgroundColor: active ? 'green' : 'grey' />; // XXX: Components withWires can optionally specify an optional// "key" prop, which aids in programmatically interpreting// the resultant dataflow diagram.const WiredClock = ;const WiredInverter = ;const WiredLightEmittingDiode = ; // XXX: Components wrapped withDataflow are passed a "subscribe" prop,// which is used to listen to changes in the dataflow diagram state. { const clk = ; const nClk = ; // XXX: Register to listen to signals *after* the initial layout, // as the digram requires an initial render pass before any // wiring or value propagation can be properly determined. ; return <div className="App"> <WiredClock cout=clk /> <WiredInverter input=clk output=nClk /> <WiredLightEmittingDiode someOtherPropThatWillBeHandledLikeUsual active=nClk /> </div> ;} App;
🔂 Iteration
It is also possible to iterate across react-dataflow
diagrams. Iterators effectively suggest a most sensible sequence of elements and wires to step through in order to satisfy the rules of dataflow. For example, the Classical
sequence type returns an array of phased execution which follows the rule that an element on the diagram cannot be executed until all of its inputs have been satisfied.
; console; // Returns an array of consecutive phases of linear execution.