author: Krasimir Tsonev

Hi there, I'm . Senior front-end engineer with over 13 years of experience. I write, speak and occasionally code stuff. Follow me on Twitter, GitHub, Facebook or LinkedIn

Part 2: Riew - reactive view basics

Part 2: Riew - reactive view basics Photo by Stephanie Harvey

Riew is a library based on communicating sequential processes (CSP). It is made to help with communication and synchronization between your view and business logic. It's distributed as a npm package, it has 0 dependencies and adds ~8KBs (gzip) to your application on production.

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:

Routines and Channels

The routine in Riew is a JavaScript generator function. We can run it by passing it to the go method.

import { go } from 'riew';

function * myRoutine() {
  // ...
};

go(myRoutine);

The routine is paused when we yield and resumed when the job outside of the routine is done. What exactly is that job depends on the case. We can yield a promise for example and myRoutine will be resumed once the promise is resolved.

function asyncWork() {
  return new Promise(done => setTimeout(done, 2000));
}
function * myRoutine() {
  console.log('Hey!');
  yield asyncWork();
  console.log('Bye!');
};

go(myRoutine);

We will see "Hey!" and two seconds later "Bye!". We can also use the call method to run another routine:

import { go, call } from 'riew';

function asyncWork() {
  return new Promise(done => setTimeout(done, 2000));
}
function * anotherRoutine() {
  yield asyncWork();
}
function * myRoutine() {
  console.log('Hey!');
  yield call(anotherRoutine);
  console.log('Bye!');
};

go(myRoutine);

The result is the same. We will get "Hey!", two seconds delay and "Bye!". Yielding something for Riew means that we want to perform a potentially async operation outside of the current routine.

Now imagine that we have multiple routines that are running at the same time. If they are part of the same application they will probably need to talk to each other. What if we have to deal with shared state or one of the routines expects data from other two routines. We need a tool that will allow us to exchange data between routines and synchronizes them. That tool is called channel.

In Riew we can create channels in a couple of different ways. For example by using the chan function. Or if we need a specific buffering strategy by using sliding or dropping methods.

We can put value in a channel and later take it from there. These two actions though follow specific rules. By default the channels in Riew are non-buffered which means that the put operation is a blocking operation until there is a take. Or in other words we can't push a value to the channel if there is no one to take it. The opposite is also true. We can't take from the channel if there is no value inside. Example:

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

const ch = chan();

go(function * () {
  console.log('A started');
  yield put(ch, 'foo bar');
  console.log('A finished');
});
go(function * () {
  console.log('B started');
  console.log(yield take(ch));
  console.log('B finished');
});

// Results in:
// > A started
// > B started
// > A finished
// > foo bar
// > B finished

The routine A starts and we see "A started". Then it gets blocked by the put to the ch channel. There is still no taker registered for that channel. Then in the console we see "B started". When B yields take(ch) Riew resolves the pending put and routine A continues. We see "A finished" followed by "foo bar", the value that is send through the channel. And at the end in the console lands "B finished".

We may have different types of channels. Or to be more specific channels with different types of buffers. We can for example have a sliding buffer of size 2.

const ch = chan('CHANNEL ID', buffer.sliding(2));

go(function * () {
  console.log('start');
  yield put(ch, 'A');
  yield put(ch, 'B');
  yield put(ch, 'C');
  yield put(ch, 'D');
  console.log('end');
  console.log(ch.value()); // C,D
});

In this case we will see both "start" and "end" because when using a sliding buffer the puts are non blocking operations. The size is 2 so we will get "A" and "B" buffered into the channel. Then when "C" arrives the buffer will throw away "A", will shift "B" to the first position and will add "C". At the end the values in the channel are "C" and "D".

For convenience there are aliases for the different channels with different types of buffers. For example the above code could be written as follows:

const ch = sliding(2);

go(function*() {
  console.log('start');
  yield put(ch, 'A');
  yield put(ch, 'B');
  yield put(ch, 'C');
  yield put(ch, 'D');
  console.log('end');
  console.log(ch.value()); // C,D
});

Riew

riew is a combination of a view function and routines. When we mount the riew the routines start. Each routine receives a render function which sends data to the view function. When we unmount the riew the routines are shut down.

function view(props) {
  console.log(`View rendered with ${ JSON.stringify(props) }`);
}
function * routineA({ render }) {
  render({ a: 'foo' });
}
function * routineB({ render }) {
  render({ b: 'bar' });
}

const r = riew(view, routineA, routineB);
r.mount();

The library renders the views with a microtask to avoid unnecessary calls to the view function. In this case the two render calls are batched in a single render and the result is "View rendered with {"a":"foo","b":"bar"}".

If you ever worked with React you will probably make the parallel seeing that the view function is like a React component. For convenience there is a React integration directly built-in. So if you use that you will probably never call mount, update and unmount. Those methods are fired automatically when your component is mounted, re-rendered and unmounted.

The render function is not only for raw data. We can push some values which are treated differently. If we for example send a channel, the view is automatically subscribed to it. Which means that we can update (re-render) when doing put operation on the channel.

function view(props) {
  console.log(`View rendered with ${ JSON.stringify(props) }`);
}
function * routine({ render }) {
  const ch = chan();
  render({ hey: ch });
  yield sleep(2000); // <- pauses the routine for 2 seconds
  yield put(ch, 'bar');
}

const r = riew(view, routine);
r.mount();

The idea is to have pure view function and shift the logic to routines.This code will render the view with an empty object first and two seconds later with {"hey":"bar"}. The idea is to have pure view function and shift the logic to routines.

We can of course make the communication in both direction. Not only the routine pushes data to the view but also the view sends data to the routine:

function view(props) {
    setTimeout(() => {
    props.message('Hey routine.');
  }, 2000);
}
function * routine({ render }) {
  render({
    message(str) {
      console.log(str);
    }
  });
}

const r = riew(view, routine);
r.mount();

If we run this snippet we will get Hey routine. with a two seconds delay.

Using similar approach we may write complex features that require input by the user. It's just the routine making the connection between the view and the channels in our application. To make this possible Riew offers standalone versions of put and take functions. We need such versions so we can call them from within callbacks, not only from a generator. Let's check the following example:

import { chan, take, sput, riew } from 'riew';

const ch = chan();

function* routineA({ render }) {
  const name = yield take(ch);
  console.log(`First name: ${name}`);
}
function* routineB({ render }) {
  render({
    setName: name => sput(ch, name),
  });
}
function view(props) {
  setTimeout(() => props.setName('Steven'), 2000);
}

const r = riew(view, routineA, routineB);
r.mount();

In this snippet routineA expects some data through a channel ch. routineB is the one that will push that data to the channel but it waits to be delivered from the user. In the view function we simulate a user interaction by delaying the setName call. When we mount the riew we will experience two seconds blank console and then "First name: Steven". Notice the usage of the sput function. We can't yield put because we are not in a generator context but in the context of the setName arrow function.

State

CSP as a concept doesn't have state. State in a sense of global application state which persist across the whole journey of the user. The CSP channels work in a different way. We can't really implement state by using CSP channels only. We need to abstract such feature outside of CSP.They do keep values but those values are consumed and disappear. This means that we can't really implement state by using CSP channels only. We need to abstract such feature outside of CSP. Riew does provide such abstraction. It's just build next to CSP channels.

Every Riew state can produce new channels - read (selectors) and write (mutators) channels. There is a synchronous getter and setter but I recommend using channels for these operations. Here is a silly counter example:

const counter = state(0);

go(function*() {
  console.log(yield take(counter.DEFAULT)); // 0
  yield put(counter.DEFAULT, 42);
  console.log(yield take(counter.DEFAULT)); // 42
});

To create a state we use the state function. We may or may not pass an initial value. In the example above that is 0. There is one channel DEFAULT created automatically by Riew. We can take and put to this channel to respectively get and set the value. For convenience we may skip mentioning it and pass directly the state instance like so:

const counter = state(0);

go(function * () {
  console.log(yield take(counter)) // 0
  yield put(counter, 42);
  console.log(yield take(counter)) // 42
});

We pass counter but under the hood counter.DEFAULT is used.

We may also create our own custom selectors and mutators channels. We use a take operations on selector channels and put operation on the mutator ones. For example:

const user = state({
  age: 22,
  firstName: '',
  lastName: '',
});

const whoAmI = user.select(
  data => `
    Hey, I'm ${data.firstName} ${data.lastName}
    and I'm ${data.age} years old.
  `
);
const setName = user.mutate((data, [first, last]) => ({
  ...data,
  firstName: first,
  lastName: last,
}));

go(function*() {
  yield put(setName, ['Jon', 'Snow']);
  console.log(yield take(whoAmI));
});

Here the state contains a user object. We have age, first and last name. A read channel is defined by using the select method on the state. The argument is a function that receives the state's value and must return another value. The result of the selector function depends on the use case. In this example we generate a sentence showing who the user is. If you use Redux you probably have experience creating such small helpers.

The mutate method has similar signature except that instead of selector function it expects reducer function. Again this concept is derived from Redux. The reducer is a function that receives the current state value and must return a new version. The reducers here in Riew has to meet the same requirements as in Redux so it is good to check this page and refresh your knowledge. In our example above we care only about updating the first and last name of the user. We spread the data to keep the age value then set firstName and lastName. In the end the routine is just using whoAmI and setName channels.

Reading vs taking

If you check the documentation of Riew you will find a read (and sread) method. Reading from a channel is not the same as taking from a channel. read is not consuming the value but only checks what it is and it doesn't resolve pending puts. It is passive operation.

const ch = chan();

go(function * A() {
  console.log(yield read(ch));
  console.log(yield take(ch));
});
go(function * B() {
  yield put(ch, 42);
});

Routine A starts and reaches the first yield. The read is resolved immediately with undefined because that is what the channel contains - nothing yet. The following like has a take so A pauses there. After B starts a put is made which resumes routine A. The result of this code is undefined followed by 42.

There is also a standalone version of the read method - sread. Similarly to stake and sput this function may be used outside a routine context.

const ch = sliding();

go(function* B() {
  yield put(ch, 'foo');
});

sread(ch, value => {
  console.log(value);
});

The result here is "foo". We are using a sliding buffer so we have non-blocking puts. When we call sread the channel ch already has a value. It is important to understand that after the sread the value is still in the channel.

PubSub (listening)

The reading was introduced in Riew for one specific reason - implementing the PubSub pattern. To provide a mechanism of subscribing to channel values. The PubSub pattern offers one-to-many relationship. We have one channel and many subscribers which must receive the same value when that value comes in. We can't really define publisher with many subscribers in CSP. That is why read exists.The CSP pattern breaks this rule because by definition the channel loses the value once someone takes it. We can't really define publisher with many subscribers in CSP. That is why read exists. It extends the CSP model to support the one-to-many relation between a channel and subscribers. See the following example:

const ch = sliding();

sread(ch, value => {
    console.log(value);
}, { listen: true });

go(function* B() {
  yield put(ch, 'foo');
  yield put(ch, 'bar');
  yield put(ch, 'zar');
});

You will probably never use { listen: true } but I'm showing it here so you get a better understanding what is actually happening. The sliding buffered channel allows non-blocking puts so routine B simply starts, pushes three values and ends. Because we are reading with listen: true our callback is fired three times and we get "foo", "bar" and "zar" in the console. For convenience there is a listen method and we can simplify the sread above to just:

listen(ch, value => {
  console.log(value);
});

Again we have to stress out that by using listen we are just reading the values from channels. We are not consuming them and the values are still in the channels after that.

Dealing with dependencies

Very often the question "How to pass X to Y so I can use it?" is neglected by the developers. We tend to pass dependencies directly which makes our code difficult to maintain and extend. Riew provides a canonical way of passing services, configuration, helpers or whatever else we use in our application. We can set our dependencies into a global registry and later inject them in riews and/or routines. For example a config type of data:

register('config', { theme: 'dark' });

const r = riew(function (props) {
  console.log(`
    Selected theme: ${ props.config.theme }
  `); 
}).with('config');

r.mount(); // Selected theme: dark

Or a service that reaches the routine:

const answerService = {
  getAnswer() {
    return new Promise(done => setTimeout(() => done(42), 2000));
  }
}
const view = function (data) {
  console.log(JSON.stringify(data));
}
const routine = function * ({ render, service }) {
  const number = yield service.getAnswer();
  render({ answer: number })
}
const r = riew(view, routine).with({ service: answerService });

r.mount();

{"answer": 42} will reach our view function after two seconds delay. Also have in mind that if some of the dependencies is a channel we will get a subscription and value fetching as it is passed to the render function through a routine.

There is also go.with method which works the same way. If you pass a string it tries to find the dependency in the global registry. If it is an object it gets passed to your routine. For example:

// somewhere in your code's entry point
register('config', { theme: 'dark' });

// somewhere deep
const A = function*({ config, foo }) {
  console.log(config.theme, foo);
};

go.with('config', { foo: 'bar' })(A); // "dark", "bar"

go.with is meant to be used when we run routines in a standalone fashion. When a routine is not part of a riew.

Final words

There are couple of small other things to learn but the basics are really in this article. We need to know what a routine is and how it works with channels. Once we understand this concept we will be able create reactive views (riews) and manage data flow through routines. The rest is more or less practice with the library. Check out the next part of the series to find more practical use cases.

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
If you enjoy this post, share it on Twitter, Facebook or LinkedIn. Or maybe comment below: