Petyo Ivanov
Posted on May 31, 2019
Intro
In the my recent adventure, I decided to find the one true way to manage the internal state of a complex React component. A virtualized list is a complex affair. Users scroll and load new data, items resize because images load, devices change orientation. Sometimes, all of the above happens simultaneously.
I started with a redux-like store. A couple of days later, I ended up in entangled nested reducers and a bunch of repetition I could not get rid of. The available tests could not help me figure out why the component was behaving in unexpected ways.
I was here, quite fast:
artist: Manu Cornet
There must be a better way. Or so, I thought.
Let's Try Reactive Programming! Enter RxJS
Figuring out RxJS while learning reactive programming was hard. RxJS is extremely powerful at the cost of being complex. A bunch of abstractions which did not immediately click or were named in the exact opposite way from how I understood them - hot and cold Observables, Observers, Subscribers, Subscriptions, and Schedulers, oh my. Nevertheless, I managed to plow through. Thanks to Learn RxJS and RxMarbles, the component store was re-done. (Don't dive into the resources above, remember them for later. They are long and awesome and get hard really fast. Finish this article first).
The result implementation was beautiful, 1/4 the size of the redux store, testable and reliable. Beauty is in the eye of the beholder of course. The first version of Virtuoso shipped with RxJS store implementation.
Looks awesome, they said. Not gonna use it, RxJS is huge, no way I am adding this as a dependency, they said. Sigh.
The Dunning-Kruger effect kicked in. I knew enough about reactive programming to replace RxJS with a small home-grown implementation. Better do this early, before I add more complex features.
It worked, bringing the usual amount of hard to trace bugs.
You are reinventing the wheel, they said. Check Callbag, they said. I should have asked earlier. Anyway, the home-grown solution was there, and it worked. Better is the enemy of good enough. Stop messing around and finish what you have started. Let me browse the Callbag docs really quick...
So let's cut the bullshit.
Reactive programming is programming with asynchronous data streams.
In a way, this isn't anything new. Event buses or your typical click events are really an asynchronous event stream, on which you can observe and do some side effects. Reactive is that idea on steroids. You are able to create data streams of anything, not just from click and hover events. Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc. For example, imagine your Twitter feed would be a data stream in the same fashion that click events are. You can listen to that stream and react accordingly.On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams. That's where the "functional" magic kicks in. A stream can be used as an input to another one. Even multiple streams can be used as inputs to another stream. You can merge two streams. You can filter a stream to get another one that has only those events you are interested in. You can map data values from one stream to another new one.
There we go. That's the starting point I think everyone needs. This is what inspired me to write this post.
What follows is what I consider the bare minimum of what you need to understand about reactive programming, presented as jest tests. I will stick to things which can be useful in React projects, where binding to a DOM element events directly does not make much sense. This is the full test suite - let's go through each test. We are using Callbag as the underlying implementation. However, all of the above would look mostly the same with RxJS. Don't worry if you don't know Jest. The assertions should be self-explanatory.
Subject, Subscriber and Observe
test("subject emits values to its subscribers", () => {
const a = subject();
const subscriber = val => expect(val).toEqual("foo");
observe(subscriber)(a);
// Ignore the "1" parameter for now
a(1, "foo");
});
This pretty much captures the whole idea of reactive programming. In the above, we:
- create a stream
a
, which in our case is a generic subject; - create a subscriber, which acts on the values coming from the stream(in this case, we verify that we received the correct value);
- we attach the subscriber to the stream using
observe
; - we push
"foo"
in the streama
;
If the above makes sense to you, congratulations! The rest of the examples just add small blocks on top of that.
Behavior Subject
test("behavior subject emits previously pushed values to new subscribers", done => {
const a = behaviorSubject("foo");
a(1, "bar");
const subscriber = val => {
expect(val).toEqual("bar");
done();
}
observe(subscriber)(a);
});
Next, we have the behavior subject. Its behavior is very similar to the vanilla subject, with one exception - the behavior subject is stateful. This means that attached subscribers will immediately be called with the last value of the subject. Also, it is constructed with an initial value. In a nutshell, subscribing to such subject means that you will be called immediately.
Behavior subjects are prevalent in the Virtuoso store implementation - that's where it keeps most of its state.
Pipe and Map
test("pipe and map transform the incoming stream values", done => {
const a = subject();
const subscription = val => {
expect(val).toEqual(2);
done();
};
const b = pipe(
a,
map(i => i * 2)
);
observe(subscription)(b);
a(1, 1);
});
Passing values around is not much fun. Thanks to pipe
and map
, we create an output stream (b
) which transforms and emits values coming from a
.
Combine
test("combine takes values from two streams", done => {
const a = subject();
const b = subject();
const subscription = vals => {
expect(vals).toEqual([1, 2]);
done();
}
const c = pipe(combine(a, b))
observe(subscription)(c)
a(1, 1);
b(1, 2);
});
test("pipe, combine and map work together", done => {
const a = subject();
const b = subject();
const subscription = val => {
expect(val).toEqual(3);
done();
}
const c = pipe(
combine(a, b),
map(([aVal, bVal]) => aVal + bVal )
)
observe(subscription)(c)
a(1, 1);
b(1, 2);
});
combine
is what I consider the last essential tool we need. It allows you to build an output stream which transforms incoming values from two or more input streams.
Bonus - scan
and sampleCombine
The test suite includes two more tests that show how scan
and sampleCombine
work. If the examples so far make sense to you, you should have little trouble figuring them out.
Stay tuned for the next post, where we are going to build a simple store from subjects and integrate it with a React component. We will also talk about why the heck would one need to do that when you can use hooks, Redux, etc.
In the meantime, go through The introduction to Reactive Programming you've been missing from André Staltz. You can also fork the sandbox and add more tests for the some of callbag utilities listed in the Callbag Wiki. Share your forks in the comments!
Posted on May 31, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.