author: Krasimir Tsonev

Hi there, I'm . Senior front-end engineer with over 13 years of experience. I write, speak and occasionally code stuff.

Part 3: Riew - reactive view in patterns

Part 3: Riew - reactive view in patterns Photo by Ricardo Gomez Angel

In this article we will see some use cases of Riew. Those are patterns which I found repeating while using the library. There are other blog posts part of the same series and I will suggest checking at least this one so you get a basic understanding before jumping into the code snippets below.

This blog post is part of the series introducing Riew library. Reactive library that aims to solve communication and synchronization issues between your view and business logic.

  1. We need channels! A gentle introduction to CSP
  2. Riew - reactive view basics
  3. Riew - reactive view in patterns

Table of Contents

(Live demo of all the scripts below could be found here. Feel free to change the code and experiment with the final result.)

Channels - merging/piping, strategies, transforms

You may be interested in checking the official docs of chan, go, put, take, sread, merge, listen methods.The channels are the primary tool for communication and synchronization in a Riew based application. It's essential to understand their full potential. In Riew merging and piping of channels are operations that overlap. In a sense that when we merge we actually pipe values from multiple channels to a single one. We may also pipe the values of two or more channels to a function and generate a single value so that is a bit like merging. That is why we use merge and pipe verbs interchangeably.

Merging is consuming values

import { merge, chan, go, take, put } from 'riew';

const chA = chan();
const chB = chan();
const ch = merge(chA, chB);

go(function * A() {
  console.log(yield take(ch)); // foo
  console.log(yield take(ch)); // bar
  console.log(yield take(ch)); // zoo
});
go(function * B() {
  yield put(chA, 'foo');
  yield put(chB, 'bar');
  yield put(chA, 'zoo');
});

The merge function internally defines a looping take. This means that there is always a taker in place for chA and chB channels. Routine A is effectively paused every time when it reaches take and resumed when routine B pushes values.

One-shot piping using take

Instead of merge we may use take if we need to have merging happening just once.

const chA = chan();
const chB = chan();

go(function * A() {
  const value = yield take([chA, chB])
  console.log(value); // ['foo', 'bar']
});
go(function * B() {
  yield put(chA, 'foo');
  yield put(chB, 'bar');
});

Notice that here take waits for both channels to receive values.

Merging using listen

The listen method offers an interesting merging capabilities.

const chA = chan();
const chB = chan();

listen([chA, chB], value => {
  console.log(value);
});

sput(chA, 'foo');
sput(chB, 'bar');
sput(chB, 'zoo');

The result of this snippet is ["foo, bar"] followed by ["foo, zoo"]. That is because Riew waits initially for both channels to have values and then resolves the callback for the first time. After that every put on any of the channels fires the callback. We now have channels fulfilled. Remember that listen is not consuming the value from the channel.

Strategies

take, read and listen accept additional options. All of the options could be seen in the official docs. We will focus on the strategy one here. The default value of this setting is ALL_REQUIRED which means that if we specify multiple channels Riew will wait till all the channels have values. As an alternative we may use ONE_OF strategy to change this behavior. Let's take the last example, change it and see the result:

const chA = chan();
const chB = chan();

listen([chA, chB], (value, idx) => {
  console.log(value, idx);
}, { strategy: ONE_OF });

sput(chA, 'foo');
sput(chB, 'bar');
sput(chB, 'zoo');

The resulted value inside the callback is not an array anymore. It is a "foo" string once, then "bar" and at the end "zoo". It is just our callback fired every time when there is something new in any of the channels. The idx second parameter is showing us which of the channels received that value.

Routines - start, stop, re-run, fork

Routine's lifecycle

You may be interested in checking the official docs of go, call, fork methods.Each routine is a generator function and as such there is code which iterates over the created generator object. Because this code lives in Riew the library has the power to control the routine's lifecycle and so there is an API for this.

To run a routine we use the go function. This function calls our generator function and operates with the returned generator object.

import { go } from 'riew';

function * myRoutine() {
  console.log('Hello world');
}
go(myRoutine); // "Hello world"

Before to see how to stop a routine we have to clarify what a stopping means. That is when we yield something the function is paused and then never resumed. This means that the routine is stopped. And the routine may stop for a couple of reasons. We call its stop method, we yield stop or it gets automatically terminated because it is part of a unmounted riew. Here is an example that stops the routine after two seconds:

function * myRoutine() {
  console.log('Hello');
  yield sleep(2000);
  console.log('world');
}

const routine = go(myRoutine);

setTimeout(() => {
  routine.stop();
}, 1000);

"world" in this example will never appear in the console. That's because myRoutine will never be resumed after the yield sleep call.

Re-running/looping routine

Re-running a routine means terminating the current iterating and running the generator again.

As we saw above the go function returns an object that has a method stop. We also have rerun. Once we call it the routine will be restarted.

const routine = go(function * () {
  console.log('Hello!');
  yield sleep(1000);
  console.log('Bye!');
});
routine.rerun();

We'll see "Hello" twice before seeing "Bye", because the routing will be restarted before it gets resumed from the sleep.

Another way to restart the routine is to return the go function. For example:

go(function * () {
  console.log('Hello!');
  yield sleep(1000);
  console.log('Bye!');
  return go;
});

This routine will print "Hello!", will wait a second and will print "Bye!". And will do that in a endless loop because the generator is restarted every time.

Delegating and forking

Sometimes we want to spread our business logic in multiple routines. We can organize them by using the call and fork methods. call is simply delegating the current execution to another routine while fork runs routines in parallel.

function * delegation() {
  yield sleep(1000);
  console.log('Wait for me');
}
function * parallel() {
  yield sleep(1000);
  console.log('I run in parallel');
}
function * main() {
  console.log('A');
  yield call(delegation);
  console.log('B');
  yield fork(parallel);
  console.log('C');
}
go(main);

And the result of this code is:

A
Wait for me
B
C
I run in parallel

Notice how C is before I run in parallel. That's because parallel routine is forked and it doesn't block main generator.

Local and application state

I'm sure Riew can be easily paired with every framework that renders stuff. However, I have made the library with the idea to consume it in my context - React. That is why I will mention React a little bit more here in this section.

Use the routine for a short-life local state

You may be interested in checking the official docs of state, riew, react methods.React supports state on a local level from day one. We know the setState class method and the useState hook. They both provide tooling for local state management. Riew however suggests shifting that management. Instead of keeping it inside the view/component we place it outside, in routines. Consider the following example:

const Component = function ({ value, increase, decrease }) {
  return (
    <Fragment>
      <p>{ value }</p>
      <button onClick={ increase }>plus</button>
      <button onClick={ decrease }>minus</button>
    </Fragment>
  );
}
const routine = function * ({ render }) {
  let counter = 0;

  render({
    value: counter,
    increase() {
      counter += 1;
      render({ value: counter });
    },
    decrease() {
      counter -= 1;
      render({ value: counter });
    }
  })
}
const R = riew(Component, routine);
ReactDOM.render(<R />, ...)

The routine starts when the component is mounted and dies when the component is unmounted. It doesn't run multiple times (unless we force such behavior). This allows us to specifically control when the render happens and here we do it couple of times with different props. Riew merges the objects that we pass to render and aggregates the final result. We render once to send the initial value of the counter and both functions increase and decrease. Then later when some of the buttons is clicked we rerender with a new counter value.

The above example demonstrated managing state by using simple variables. We can achieve the same result with the Riew's state and channels capabilities.

Managing application state

First, we don't want to create our state inside the routines. The state should be created/initialized at the root of our application. Then it comes the question how we will reach it. The answer is "we don't". In most of the cases we don't want to use the state directly. We want to use the channels that are connected to it - selectors and mutators. And because the channels could be exported to the global registry we may use the with method to access them.

// somehwere at the root of the application
const counter = state(0);
counter.select(value => `Counter: ${ value }`).exportAs('counter');
counter.mutate((previous, payload) => previous + 1).exportAs('inc');
counter.mutate((previous, payload) => previous - 1).exportAs('dec');

// somewhere deep in our application tree
const Component = function ({ value, increase, decrease }) {
  return (
    <Fragment>
      <p>{ value }</p>
      <button onClick={ increase }>plus</button>
      <button onClick={ decrease }>minus</button>
    </Fragment>
  );
}
const routine = function * ({ counter, inc, dec, render }) {
  render({
    value: counter,
    increase: () => sput(inc),
    decrease: () => sput(dec)
  })
}
const R = riew(Component, routine).with('counter', 'inc', 'dec');
ReactDOM.render(<R />, ...)

select and mutate define channels. We expose them to the registry with the exportAs method. Later we define a riew and say that we want these three channel counter, inc and dec channels injected. They reach the routine where we prepare an object suitable for our Component. Once some of the buttons is clicked we put on either inc or dec channels. This action results in a state change which triggers a re-render. The re-rendering happens because the component is hooked to the counter state via its value prop.

Async mutations

The reducer that we pass to the mutate function may be also a routine (generator function). This means that we may use it to get data from other state channels or to perform an async call before changing the value.

const url = state(null);

const setURL = url.mutate(function * A(current, what) {
  if (what === 'kitty') {
    const api = `https://aws.random.cat/meow`;
    const { file } = yield fetch(api).then(res => res.json())
    return file;
  }
  return null;
});

listen(url, value => {
  console.log(`URL changed to ${ value }`);
});

go(function * B() {
  console.log('B starts');
  yield put(setURL, 'kitty');
  console.log('B ends');
});

The channel setURL hides the async call. And because it is a state mutator channel every time when we push to it the routine A will be executed. Another nice side effect of such mutators is that they block the puts. This example results in first seeing "B starts" then "URL changed to ..." and then "B ends". That is because routine B is paused until the mutation is over.

Bridging the non-riew with the riew world

You may be interested in checking the official docs of stake, sread, sput, close methods.It's not good to be vendor locked to a specific framework. Riew makes the assumption that you have code that is not working with channels and routines. That is why provides standalone version of put, take, close and read. Those version start with the letter s in front and receive a callback which indicates when the operation is finished. Check out the following example:

const auth = chan();
const print = chan();

const user = {
  name: 'Steve',
  logged: false
}

stake(auth, (isLogged) => {
  user.logged = isLogged;
})
listen(print, () => {
  console.log(`Steve is logged ${ user.logged ? 'in' : 'out'}`);
});

sput(print);
sput(auth, true);
sput(print);

The result is "Steve is logged out" followed by "Steve is logged in".

Handling errors

Handling errors inside routines

The code inside the routine may fail either in the routine itself or outside when we yield stuff. In both cases we may use try-catch blocks to deal with such failures.

const IAmTheProblem = () => {
  throw new Error('Ops!');
};
const IAmTheFutureProblem = () => {
  return new Promise((done, rejected) => {
    setTimeout(() => rejected(new Error('Async Ops!')), 1000);
  });
}

go(function * () {
  try {
    IAmTheProblem();
  } catch(error) {
    console.log(`Error caught: ${ error.message }`);
  }
  try {
    yield IAmTheFutureProblem();
  } catch(error) {
    console.log(`Error caught: ${ error.message }`);
  }
});

When the error happens in an async process Riew resumes the generator with generator.throw so we can catch it.

Handling errors in state selectors and mutators

State selectors and mutators are another source of errors. In this case Riew provides an API for handling such errors.

const s = state('foo');

const change = s.mutate(
  function(value, newValue) {
    if (newValue === 'a-ha') {
      throw new Error('foo');
    }
    return value;
  },
  error => console.log(`Error: ${ error.message }`)
);

sput(change, 'a-ha');

Both the select and mutate method of the state accept a second argument - a function which is fired with an error occurs while running the selector or mutator.


As we mentioned in the beginning of this article - live demo of all the scripts above could be found here. Feel free to change the code and experiment with the final result.

This blog post is part of the series introducing Riew library. Reactive library that aims to solve communication and synchronization issues between your view and business logic.

  1. We need channels! A gentle introduction to CSP
  2. Riew - reactive view basics
  3. Riew - reactive view in patterns