Reactive view - the concept
Something bugs me last couple of years. I'm using React for some time now and there is always this doubt if I'm placing the business logic on the right place. I'm trying to be pragmatic, to follow best practices and listen what the community is saying. However, I still feel that something is not ok. This blog post presents the idea of the reactive view. That is nothing new per se but I came up with this term because it fits well in my idea.
Where we write the business logic in our React applications? Well, back in the days when Flux was around we were doing kind of heavy action functions. We did call them action creators but the code inside was more then just dispatching an action. It was using services, doing async calls, normalizing data and eventually yes, dispatching actions. There was some data manipulations inside the stores but most of the domain logic was in the so called action creators. Then later Redux came and we started using it. Very quickly we realized that it doesn't provide a mechanism for dealing with async operations. So, we came up again with the idea of putting more logic into action creators or simply pass around the Redux store's dispatch
method. There were many solutions to that problem but looks like two libraries made it to the top - redux-thunk and redux-saga which if I'm not mistaken use Redux middleware. Today I'm seeing more and more developers shift away from Redux and and its family in favor of the hooks API. The result of this is that our components become more then just UI representation. The now contain business logic.
Aren't we abusing the React APIs and shall we really put so much business logic inside the components?I myself wrote some horrible components with 4+ useEffect
hooks and 2 custom ones. After a day or two it gets really difficult to understand what is going on. Sure I could refactor them but I started questioning if that is the right approach. Aren't we abusing the React APIs and shall we really put so much business logic inside the components? 🤔
The idea
Then I started wondering wouldn't be cool if I take what I liked in Redux and redux-saga and come up with something "better". I'm professional wheel re-inventor and time waster so why not. I very much like creating useless tools that no one find interesting. That is how I started forming the idea about Riew. I have made the name out of the phrase "reactive view".
My goal with this project is to keep my React components (my views) away from the complex business logic. They can still use hooks and all but for their internal processes. The domain specific logic which touches the global application state should happen elsewhere.
The thing which I mentally do in my head all the time is to imagine my view layer and a process next to it. While this process is running it does whole a lot of stuff. Gets input from the view, touches app state, does async calls etc. At some point it triggers also re-rendering of the view. That process I called "routine". Inspired by goroutines in Go language. I decided to use generators to represent the routine. It is because in some sense the environment that runs the generator controls the execution flow which is really convenient for what I want to achieve.
So, I had a view (React component) and a routine (generator function). They had to communicate. But how? I was exploring (and continue doing so) the communicating sequential processes (CSP) concept. For a first time I saw a pattern which provides so interesting mechanisms for synchronization of two (and more) processes. It deals with communication too but that is not the main thing. There was a natural decision that I will use this new shiny pattern in my library. Bringing the CSP channels in I completed the puzzle:
- A view that deals with only UI.
- One or more routines that start when the view is mounted and die when the view is unmounted.
- CSP channels for communication and synchronization between view, state and routines.
Example
I will close this blog post with a simple example. Its purpose is not to teach you about the Riew library. There are other three articles on this topic. But to present to you a slightly different model of thinking.
Let's say that we have a channel and a view (React component) with a button and an image placeholder.
import { sput, chan } from 'riew';
import { riew } from 'riew/react';
const ch = chan();
const View = function({ kitty }) {
return (
<React.Fragment>
<small>When you click the button wait a bit. There is a HTTP request happening in the background.</small>
<button onClick={ () => sput(ch) }>I want Kitty</button>
{ kitty && <img src={ kitty } width="250" /> }
</React.Fragment>
);
}
When we click the button we expect to make a request to remote server, get a URL of a kitty and place it back to the component. We need a routine that will pick up that click and will trigger the request. Above we are doing sput(ch)
which means that we put something into the channel ch
. What this something is depends on the use case. Here we pass nothing because we only want to control flow. We use sput
, a standalone version of put
because we are not in a routine context. We can't yield
in a React component.
Here is how our routine looks like and how we wire it to the view:
import { sput, take, go, chan } from 'riew';
import { riew } from 'riew/react';
const ch = chan();
const View = function({ kitty }) { ... }
function * routine({ render }) {
yield take(ch);
const kitty = yield getKitty();
render({ kitty });
return go;
}
const R = riew(View, routine);
ReactDOM.render(<R />, document.querySelector('#output'));
async function getKitty() {
const { file } = await fetch('https://aws.random.cat/meow').then(res => res.json());
return file;
}
When we render <R />
our View
is mounted and so the routine starts. It gets suspended though at the first yield take
. That is because it waits someone to put
into the ch
channel. When we click the button the routine continues and yields the result of getKitty
function which is a promise. Once the promise is resolved Riew resumes the generator and we reach the render
call. render
is a function that comes to every routine and it points to the view which the routine is attached to. In this case that is View
. kitty
is a string and lands in our component which displays the image. The return go
at the end tells to Riew that we want to rerun the routine once it finishes. The animated gif below demonstrates visually the whole process.
Here is a live demo of this example 👉 poet.krasimir.now.sh/e/nkNMjJokQIL.
If you are interested in Riew and want to learn more check the Riew series: