Part 3: Riew - reactive view in patterns
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.
- We need channels! A gentle introduction to CSP
- Riew - reactive view basics
- Riew - reactive view in patterns
Table of Contents
- Channels - merging/piping, strategies, transforms
- Routines - start, stop, re-run, fork
- Local and application state
- Bridging the non-riew with the riew world
- Handling errors
(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 put
s. 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.
- We need channels! A gentle introduction to CSP
- Riew - reactive view basics
- Riew - reactive view in patterns