RxJS and React go together like chocolate and peanut butter: great individually but they become something incredible when put together.
A quick search on npm will find a slew of hooks to connect RxJS Observables to React components, but let’s start at the beginning, because RxJS and React fit very well together “as is” because they follow the same philosophy and have very compatible APIs.
A quick aside about Why RxJS
2019 has been the year of RxJS, blowing up across the web-dev community with events like rxjs.live and ng-conf. More and more developers are finding out that RxJS is awesome and it is totally worth climbing your way through the somewhat steep learning curve.
Angular devs have been using RxJS for a while now. A quick search will find vue-rx, ember-rx, and even Svelte can use RxJS Observables as stores by default. When you learn RxJS you are learning a highly portable skill that can be used across frameworks. The concepts of Rx can actually be used across languages and platforms.
RxJS is a mature, battle hardened library for dealing with events and data flow. It is definitely going to be valuable to familiarize yourself with how it works.
Let’s start with a simple example:
We have a simple List
component here that just lists the strings it is given:
const source = ['Adam', 'Brian', 'Christine'];
function App() {
const [names, setNames] = useState(source);
return (
<div className="App">
<h1>RxJS with React</h1>
<List items={names} />
</div>
);
}
(follow along on CodeSandbox!)
Now, let’s pull those values from an RxJS Observable.
Let’s start by creating an Observable with the RxJS of()
function.
We’ll need to:
- add
rxjs
as a dependency (npm i rxjs
,yarn add rxjs
or however you need to if you're not using CodeSandbox) - import
of
fromrxjs
Then let’s create an Observable called names$
, whose value is the source
array:
import { of } from 'rxjs';
const source = ['Adam', 'Brian', 'Christine'];
const names$ = of(source);
FYI: I will be following the convention of naming an Observable variable with a \$ suffix (aka Finnish Notation), which is completely optional but I think it may help for clarity while learning.
Now what we want to do is synchronize the component state with the state from the Observable. This would be considered a side-effect of the React function component App
, so we are going to use the useEffect()
hook, which we can import from react
.
Inside the useEffect()
callback we will:
- subscribe to the
names$
Observable with thesubscribe()
method, passing our "state setter function"setNames
as the observer argument - capture the
subscription
returned fromobservable.subscribe()
- return a clean-up function that calls the subscriptions
.unsubscribe()
method
function App() {
const [names, setNames] = useState();
useEffect(() => {
const subscription = names$.subscribe(setNames);
return () => subscription.unsubscribe();
});
return (
<div className="App">
<h1>RxJS with React</h1>
<List items={names} />
</div>
);
}
Which at this point should look something like this:
The concepts and APIs in RxJS and React are very compatible: the way useEffect
aligns with an RxJS subscription and how the clean-up call is a perfect time to unsubscribe. You’ll see a lot more of that "symbiosis" as we go on.
An aside about useEffect
When using useEffect
to synchronize component state to some "outer" state, you must decide what state you want to sync with.
- All state
- No state
- Some select pieces of state
This is represented in the deps
array, which is the second argument passed to useEffect
.
To use an quote from Ryan Florence:
The question is not "when does this effect run" the question is "with which state does this effect synchronize with"
— Ryan Florence (@ryanflorence) May 5, 2019
useEffect(fn) // all state
useEffect(fn, []) // no state
useEffect(fn, [these, states])
useEffect(fn) // all state
useEffect(fn, []) // no state
useEffect(fn, [these, states])
So, in this instance we don’t have any props or other state to sync with: we just want our names array to be whatever is the current value of our Observable. We just want to update our component state whenever the Observables value changes, so we’ll go with No State and throw in an empty array []
as the second argument.
useEffect(() => {
const subscription = names$.subscribe(setNames);
return () => subscription.unsubscribe();
}, []);
Creating a custom hook
It looks like we’ll be using this pattern a lot:
- subscribing to an Observable in
useEffect
- setting the state on any changes
- unsubscribing in the clean-up function
…so let’s extract that behaviour into a custom hook called useObservable
.
const useObservable = observable => {
const [state, setState] = useState();
useEffect(() => {
const sub = observable.subscribe(setState);
return () => sub.unsubscribe();
}, [observable]);
return state;
};
Our useObservable
hook takes an Observable and returns the last emitted value of that Observable, while causing a re-render on changes by calling setState
.
Note that our state is initialized as undefined
until some value is emitted in the Observable. We’ll use that later, but for now, make sure the components can handle when the state
is undefined
.
So we should have something like this now:
Of course, we could, and probably should, have useObservable()
defined as an export from a module in its own file because it is shareable across components and maybe even across apps. But for our simple example today, we’ll just keep everything in one file.
Adding some asynchronicity
So we have this list of names showing now, but this is all very boring so far, so let’s do something a little more Asynchronous.
Let’s import interval
from rxjs
and the map
operator from rxjs/operators
. Then, let’s use them to create on Observable that only adds a name to the list every second.
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
const source = ['Adam', 'Brian', 'Christine'];
const names$ = interval(1000).pipe(map(i => source.slice(0, i + 1)));
Neat. So we can see our list appearing one at a time. Sort of useless, but off to a good start. 😄
Fetching some data
Instead of our source
array, let’s fetch the list of names from an API.
The API endpoint we’ll be using comes from randomuser.me, which is a nice service for just getting some made up user data.
We’ll add these 2 helper variables, api
and getName
which will allow us to fetch 5 users at a time and the function will help extract the name from the user data randomuser.me provides.
const api = `https://randomuser.me/api/?results=5&seed=rx-react&nat=us&inc=name&noinfo`;
const getName = user => `${user.name.first} ${user.name.last}`;
RxJS has some great utility functions for fetching data such as fromFetch
and webSocket
, but since we are just getting some JSON from an ajax request, we’ll be using the RxJS ajax.getJSON
method from the rxjs/ajax
module.
import { ajax } from 'rxjs/ajax';
const names$ = ajax
.getJSON(api)
.pipe(map(({ results: users }) => users.map(getName)));
This will fetch the first 5 users from the API and map over the array to extract the name from the name.first
and name.last
property on each user. Now our component is rendering the 5 names from the API, yay!
It’s interesting to note here, that since we moved our code into a custom hook, we haven’t changed the component code at all. When you decouple the data from the display of the component like this, you get certain advantages. For example, we could hook up our Observable to a websocket for live data updates, or even do polling in a web-worker, but the component doesn’t need to change, it is happy rendering whatever data it is given and the implementation of how the data is retrieved is isolated from the display on the page.
Aside about RxJS Ajax
One of the great benefits of using the RxJS ajax module (as well as fromFetch), is that request cancellation is built right in.
Because our useObservable
hook unsubscribes from the Observable in the clean-up function, if our component was ever “unmounted” while an ajax request was in flight, the ajax request would be cancelled and the setState
would never be called. It is a great memory safe feature built in without needing any extra effort. RxJS and React working great together, out of the box, again.
Actions
So now we have this great custom hook for reading state values off an Observable. Those values can come from anywhere, asynchronously, into our component, and that is pretty good, but React is all about Data Down and Actions Up (DDAU). We’ve really only got the data half of that covered right now, what about the actions?
Read Part 2, where we’ll explore Actions, how we model our RxJS integration after the built-in useReducer hook, and much much more.
If you have any questions, feel free to post in the comments, or you can join our Bitovi community Slack at https://bitovi.com/community/slack, and ask me directly. There are lots of other JavaScript experts there too, and it is a great place to ask questions or get some help.