DEV Community

Petyo Ivanov
Petyo Ivanov

Posted on

Reactive Programming for React Developers Part 2 - Integrate with React

In the first part of the series, we went through some basics of reactive programming. Today, we are going to implement a small (but interesting enough) task using React as the view and a reactive engine as the backend.

First Take: Counter

As a warm-up, we are going to do something similar to the Redux Counter example - a button which clicks and increments a value. Let's create a function which creates an input stream for the button click and an output stream for the counter:

import subscribe from 'callbag-subscribe'
import subject from 'callbag-subject'
import pipe from 'callbag-pipe'
import scan from 'callbag-scan'
import startWith from 'callbag-start-with'

export function createEngine() {
  const increments$ = subject()  

  const counter$ = 
    pipe(
      increments$,
      scan( acc => acc + 1, 0),
      startWith(0)
    )

  return {
    counter$,
    increments$
  }
}

There we go. If you went through the first part of the series, the above should not look that scary. We use the scan operator to capture and accumulate the clicks counter. We specify the initial value of the counter using startWith. Let's connect it to React:

import React, { useState } from 'react';
import { render } from 'react-dom';

import { useCallbagInput, useCallbagOutput } from './CallbagHooks'
import { createEngine } from './engine'

const App = () => {
  const [ engine ] = useState(createEngine)
  const buttonClick = useCallbagInput(engine.increments$)
  const counter = useCallbagOutput(engine.counter$)

  return <div>
    <button onClick={buttonClick}>Click me</button>

    <span>Button was clicked {counter} times</span>
  </div>
}

render(<App />, document.getElementById('root'));

We put the streams in the state of the component (leaving it read-only), and connect them to React using the useCallbagInput / useCallbagOutput hooks, respectively. Let's see it in action!

Notice: you can examine the hooks implementation in the example above - it is the glue between the streams and React's state. The useCallbagInput is not even a real hook.

The above approach looks like an overcomplication - you can achieve the same with useState or useReducer in fewer, simpler lines of code. However, it accomplished something important - it encapsulated the logic of our app in a building block which resides outside of our React components. You can easily write tests against it, without any React component/rendering involved.

Next, let's try something more complex!

Second step: Calculator

We will build a calculator which sums two or more numbers and keeps track of the previous sums. Check the following prototype for a better idea:

Let's see what are the requirements for our engine:

We need:

  • something to process the clicks of the number buttons
  • something to process the click of the 'sum' button

and

  • something to update the numbers to be summed
  • something to update the calculations so far

From the engine's point of view, these are two input streams and two output streams. The input streams push data into the store (numbers, sum); the output streams output the results to the consumer (in our case, the React UI). Thinking in Redux terms (although not exact mapping), the input streams are the actions, while the output streams are the state. Don't get hung up on this parallel though.

Build the Engine

import subject from "callbag-subject"
import pipe from "callbag-pipe"
import map from "callbag-map"
import scan from "callbag-scan"
import buffer from "callbag-buffer"
import cut from "callbag-cut"

const numbersToSumString = numbers => numbers.join('+')

const sum = numbers => numbers.reduce((a, b) => a + b)

export const createEngine = () => {
  const numbersToSum$ = subject();
  const calculate$ = subject();

  const solutions$ = pipe(
    numbersToSum$,
    buffer(calculate$),
    map(numbers => `${numbersToSumString(numbers)}=${sum(numbers)}` ),
    scan((solutionsSoFar, solution) => [solution, ...solutionsSoFar], [])
  )

  const pendingNumbers$ = pipe(
    numbersToSum$,
    cut(calculate$),
    map(numbersToSumString),
  )

  return {
    // input
    numbersToSum$,
    calculate$,

    // output
    solutions$,
    pendingNumbers$
  }
}

We finally got to the fun parts! We combine the two input streams (numbersToSum$ and calculate$) in different ways in order to build our output streams - the calculated solutions and the numbers in the current unfinished solution.

The part which I appreciate the most about the implementation is that the engine is stateful, but we don't deal with that manually. Instead, we use the scan, buffer and cut operators to the job for us.

The next example connects the engine to the React view we started with:

In addition to the hooks from the Counter example, we put the engine in a context and then access the streams we need in the child components. Notice that, unlike Redux, the streams do not change over time. Instead, they act like permanent pipes which take care of accepting inputs from events in the various parts of the app and delivering the updated values where necessary.

Why Callbag and not RxJS?

The engine implementation would look mostly the same if we used RxJS. For the purposes of the tutorial, callbag felt simpler (everything is a function!).

Why should I care about that? React already has hooks, Redux, MobX, etc.?

Indeed - however, consider this more of a thought-provoking exercise on how we can program outside of the framework. Compared to the traditional imperative approach, coding your logic with streams feels like programming on a higher level. Notice how the implementation above has zero if statements, no variable reassignments, and no loops. Instead, we have a few pure functions composed with pre-made operators.

I want to learn more!

An excellent resource to get you excited is RxMarbles - without any actual code, it shows the power of the Rx observables. Most, if not all of the Rx operators have their counterparts implemented in Callbag.

Top comments (0)