Cover image by Wu Yi on splash.
This article was originally posted on Spanish in my blog
This article is not a tutorial about React nor about RxJS, but just some of my thoughts about how to use RxJS on React applications.
RxJS is a library for Functional Reactive Programming (FRP from now on) in JavaScript. If you google what is FRP, you'll probably find a lot of really cool definitions, each one a little bit more complex than the previous one.
My favorite definition of FRP is:
The essence of FRP is to specify the dynamic behavior of a value when declaring it
– Heinrich Apfelmus
Mind-blowing right?
What does this mean?
When doing FRP we try to specify how the value of a variable is going to change over time at declaration time. It might seem rather strange how code like this would look, since JavaScript doesn't have primitives for doing something like this (generators is the closest thing I can think of).
On React land there IS a way to define the value of a variable that could satisfy this definition, but with some limitations. Consider the following example:
const greeting = React.useMemo(() => `${greet}, ${name}!`, [greet, name]);
useMemo
lets us define a computed value that will be recalculated whenever their dependencies change. In our case, the value of greeting
will be recomputed depending on the values of greet
and name
. Fair enough, greeting
is just the result of a simple expression `${greet}, ${name}!`
, but it turns out we control when its value is recomputed by using useMemo
, which is convenient for our definition of reactivity.
Wonderful! And that could be all, folks, and we'd live happily ever after. However, useMemo
only lets us define greeting
when greet
and name
change, but it doesn't provide any information about where and how those values change and how they are updated.
The million dollar question is: How and where do these dependencies change?
Looking at a more realistic example:
import * as React from 'react';
const GreetSomeone = ({ greet = 'Hello' }) => {
const [name, setName] = React.useState('World');
const greeting = React.useMemo(() => `${greet}, ${name}!`, [greet, name]);
React.useEffect(() => {
fetchSomeName().then(name => {
setName(name);
}, () => {
setName('Mololongo');
});
}, []);
return <p>{greeting}</p>;
};
Out GreetSomeone
component receives greet
from props
and name
is the result of a promise returned by calling fetchSomeName
.
Although the definition of greeting
hasn't changed, we cannot determine just by reading it that one of the values on the dependency array comes from a Promise and that by extent, is async.
In JavaScript there are no primitives for determining the async nature of this expression (neither in React).
Observables to the rescue
Let's get away from React for a while and let's see if we can express greeting
(and fulfill our FRP definition) using RxJS. We'll start by defining two Observables that will emit the values of greet
and name
, and we will compose them to get another Observable back that will represent how greeting
changes over time:
import { combineLatest, of } from 'rxjs';
import { map } from 'rxjs/operators';
const greet$ = of('Hello');
const name$ = of('World');
const greeting$ = combineLatest(greet$, name$).pipe(
map(([greet, name]) => `${greet}, ${name}!`)
);
greeting$.subscribe(greeting => {
console.log(greeting);
});
// =>: "Hello, World!" -- When we subscribe to greeting$
In our React example the value of name
came from a Promise. In RxJS land, defining the async nature of name
is quite simple, we only have to create an Observable from fetchSomeName
and handle weather the Promise is resolved or rejected in the following way:
import { combineLatest, from, of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';
const greet$ = of('Hello');
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() => of('Mololongo')),
);
const greeting$ = combineLatest(greet$, name$).pipe(
map(([greet, name]) => `${greet}, ${name}!`)
);
greeting$.subscribe(greeting => {
console.log(greeting);
});
// ✏️: "Hello, World!" -- When we subscribe to greeting$
// ✅: "Hello, Thundercat!" -- When `fetchSomeName()` is resolved
// ❌: "Hello, Mololongo!" -- When `fetchSomeName()` is rejected
And that is all it take to define the async nature of name$
and by extent, the async nature of greeting$
.
Back to React
Considering what we know so far. How we could implement our RxJS solution in React?
To answer this question, it's convenient to first understand that useMemo
is kind of equivalent to useState
+ useEffect
. For example:
const greeting = React.useMemo(() => `${greet}, ${name}!`, [greet, name]);
Can be described like:
const [greeting, setGreeting] = useState(() => `${greet}, ${name}!`);
useEffect(() => {
setGreeting(() => `${greet}, ${name}!`);
}, [greet, name]);
Although in practice both snippets yield similar results, there are a couple of substantial differences on how they do it.
The callback function we pass to useEffect
runs after render, while the useMemo
variable is calculated before render. In other words, during the first render the value of greeting
with useMemo
will already be computed; whilst in our useEffect
version, its value on the first render will be the value defined with our useState
.
The fact we can describe a state update inside a useEffect
callback, is just pointing out that updating state is in practice a "side effect", as in it is affecting the real world. In the case of useMemo
, this is conveniently handled by React.
With that being said, the strategy to use RxJS with React is basically by deferring the way we handle these (side) effects from React to RxJS.
We'll start by copying all of our RxJS code inside our GreetSomeone
component. In order to render our component whenever greeting$
emits a value, we have to let React know that something happened by using some mechanism familiar to React, like useState
:
import * as React from 'react';
import { combineLatest, from, of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';
const GreetSomeone = ({ greet = 'Hello' }) => {
const [greeting, setGreeting] = React.useState('');
React.useEffect(() => {
const greet$ = of(greet);
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() => of('Mololongo')),
);
const greeting$ = combineLatest(greet$, name$).pipe(
map(([greet, name]) => `${greet}, ${name}!`)
);
const subscription = greeting$.subscribe(value => {
setGreeting(value);
});
return () => {
subscription.unsubscribe();
}
}, []);
return <p>{greeting}</p>;
};
After the first render (when the component "mounts"), the function we passed to useEffect
will be executed, and with that all the logic to calculate the value of greeting
.
One problem with our current solution is that if the of value of greet
changes, greeting
will not be recomputed. This is because our greet$
Observable is defined when the useEffect
callback is executed, and this just happens once. Any change to greet
will not be propagated to greet$
, and by extension neither greeting$
will know about it.
One thing we could do is add greet
as a dependency to useEffect
, making sure that the callback is executed every time greet
changes. Although this solves our problem, it could have some unexpected consequences.
The effect callback will be executed EVERY time greet
changes. When the callback runs, not only we will be defining greet$
with the latest value greet
, but also name$
will be redefined, and this will execute the getSomeName
function again.
In our initial example we are only interested in calling getSomeName
once, so let's forget about this alternative.
Something interesting about the dependency array of React hooks: The hook callback will be executed only when its dependencies change, and React tracks these changes by just doing plain old value comparison. In JavaScript, primitive values are equal when their values are equal (5
is always equal to 5
) but things like objects are only equal if they point to the same reference (memory address, call it whatever you like).
What this actually means is that if we have an object as a dependency, and the reference to that object doesn't change, it doesn't matter how the inner properties of that object change: the hook simply will not be executed. It will only run when the variable we are observing points to a different reference.
What we'll do then, is define greet$
as a BehaviorSubject
(using a ref) that will emit values whenever greet
changes:
import * as React from 'react';
import { BehaviorSubject, combineLatest, from, of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';
const GreetSomeone = ({ greet = 'Hello' }) => {
const greet$ = React.useRef(new BehaviorSubject(greet));
// Observe `greet` with `useEffect` and forward the value to `greet$`
React.useEffect(() => {
greet$.current.next(greet);
}, [greet]);
// Rest of the code remains almost the same
const [greeting, setGreeting] = React.useState('');
React.useEffect(() => {
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() => of('Mololongo')),
);
const greeting$ = combineLatest(greet$.current, name$).pipe(
map(([greet, name]) => `${greet}, ${name}!`)
);
const subscription = greeting$.subscribe(value => {
setGreeting(value);
});
return () => {
subscription.unsubscribe();
}
}, [greet$]);
return <p>{greeting}</p>;
};
BehaviorSubject
is kinda like an event emitter which we can subscribe to (just like we do with regular Observables), but as with any event emitter, we con produce values imperatively calling the next
method. We store our subject with useRef
, which lets us persist our reference between renders.
But how this is better if we have more code?
First, our main useEffect
callback just runs once: Hooray!
Second, we can hide the implementation details using a custom hook:
const useObservedValue = value => {
const subject = React.useRef(new BehaviorSubject(value));
React.useEffect(() => {
subject.current.next(value);
}, [value]);
return React.useMemo(() => subject.current.asObservable(), [subject]);
};
And then:
const GreetSomeone = ({ greet = 'Hello' }) => {
const greet$ = useObservedValue(greet);
const [greeting, setGreeting] = React.useState('');
React.useEffect(() => { /* etc */ }, [greet$]);
return <p>{greeting}</p>;
};
useObservedValue
returns the memoized value ofsubject.current.asObservable()
to discourage emitting values manually on our Subject usingnext
.
Continuing with our refactoring, we can extract the definition of name$
from the useEffect
callback (we can actually extract it from out component entirely, FWIW).
We'll also define greeting$
outside of useEffect
:
import * as React from 'react';
import { from, of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() => of('Mololongo')),
);
const GreetSomeone = ({ greet = 'Hello' }) => {
const greet$ = useObservedValue(greet);
const greeting$ = React.useMemo(
() => combineLatest(greet$, name$).pipe(
map(([greet, name]) => `${greet}, ${name}!`)
)), []
);
const [greeting, setGreeting] = React.useState('');
React.useEffect(() => {
const subscription = greeting$.subscribe(value => {
setGreeting(value);
});
return () => {
subscription.unsubscribe();
}
}, [greeting$]);
return <p>{greeting}</p>;
};
Finally, our useEffect
only responsibility is to subscribe to greeting$
and persist each emitted value with setGreeting
.
We could even encapsulate this with another custom hook:
const useObservable = (observable) => {
const [value, setValue] = React.useState();
React.useEffect(() => {
const subscription = observable.subscribe((v) => {
setValue(v);
});
return () => {
subscription.unsubscribe();
};
}, [observable]);
return value;
};
Finally:
import * as React from 'react';
import { from, of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() => of('Mololongo')),
);
const GreetSomeone = ({ greet = 'Hello' }) => {
const greet$ = useObservedValue(greet);
const greeting$ = React.useMemo(
() =>
combineLatest([greet$, name$]).pipe(
map(([greet, name]) => `${greet}, ${name}!`)
),
[greet$]
);
const greeting = useObservable(greeting$);
return <p>{greeting}</p>;
};
And that is it! We've specified the dynamic behavior of greeting$
at its definition place. You can see a working demo here.
Sorting things out
Ok, I get it. The solution I've implemented is not the cleanest and has a lot of rough edges. But, it is a good starting point to understand what it takes to use RxJS Observables in React.
Rather than using our own custom hooks, we could use a library for handling all the boilerplate. Let's take a look at the same example using rxjs-hooks:
import * as React from 'react';
import { from, of } from 'rxjs';
import {
catchError,
combineLatest,
map,
pluck,
startWith,
} from 'rxjs/operators';
import { useObservable } from 'rxjs-hooks';
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() => of('Mololongo'))
);
const GreetSomeone = ({ greet = 'Hello' }) => {
const greeting = useObservable(
input$ =>
input$.pipe(
pluck(0),
combineLatest(name$),
map(([greet, name]) => `${greet}, ${name}!`)
),
'',
[greet]
);
return <p>{greeting}</p>;
};
You can look at their documentation to understand what useObservable
does under the hood. But truth be told, the code surface is considerably reduced.
And voilá, that's all for today. By using RxJS we can express the async dynamic behavior of our state in a more declarative fashion, by using function composition and other fancy functional programming techniques.
It also lets us define really complex async logic that would be a nightmare to handle using plain old Promises.
Although there is some friction when using RxJS inside React, hooks play a great role in order to improve the way both libraries operate together.
If you liked the content, don't forget to share it on Twitter and follow me over there perhaps.
Beware: I mostly tweet JavaScript rants on Spanish.
Top comments (4)
you said: "I do not always agree with my thoughts." and I had a good laugh, thanks :-)
Nice article, Osman! I love the "learn by creating" approach!
and I surely love Rx in React posts 🙂
What do you think if we complete subject on unmount (just to be safe):
Just today I've published an article with a bit different approach to RxJS consumption in React: a fragment that observes it's RxJS children, e.g.:
It subscribes and displays in-place values from it's stream(s). Would be glad to know your thoughts!
Here's a 5min post if you're interested:
Fetching Data in React with RxJS and <$> fragment
Kostia Palchyk for RxJS ・ Aug 4 '20 ・ 5 min read
After all, do you think that using RxJS is an overkill? While the example in your post is quite simple but we have to take a lof of efforts to make it work as expected while we have useMemo and other technique which I think is much more simpler?
I think it is useful if you're working with data streaming or if you have to compose multiple sources of events that require connecting to things and cleaning up after. For everyday development, the benefits don't overcome the added complexity.