loading...
Cover image for Using React and RxJS together

Using React and RxJS together

daslaf profile image Osman Cea ・8 min read

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 of subject.current.asObservable() to discourage emitting values manually on our Subject using next.

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.

Posted on by:

daslaf profile

Osman Cea

@daslaf

Front end web developer. Father. Husband. Music lover. I do not always agree with my thoughts.

Discussion

pic
Editor guide
 

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):

const useObservedValue = value => {
    const subject = React.useRef(new BehaviorSubject(value));

    React.useEffect(() => {
        subject.current.next(value);
    }, [value]);

    // complete 
    React.useEffect(() => {
        return () => subject.current.complete();
    }, []);

    return React.useMemo(() => subject.current.asObservable(), []); // < no dep, subj is always same
};

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.:

function App(){
  return <$>You're here for { timer(0, 1000) } sec</$>
}

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: