31Jul
artwork depicting React and RxJS logo with a hook
We’re hooked!

I remember when I first got introduced to React. I was stunned by how great the idea of handling all of the presentation logic with just javascript and JSX was, and even more so: how effortless it seemed at first–you pass in props, update states. How easy!

Going through the first Tic-Tac-Toe tutorial on the React homepage felt so great, I was confident that humanity has finally found the magic bullet, at least for the front-end.

But… there was a caveat. What if we lifted the state five floors up, but some of that state and state update logic was still needed on the first floor?

How about we pass some state one level down, then some of that state one level further and so on till it reaches the component? That’s a bit laborious but ok.

Oh, I forget, I need to change the shape of the state on the fifth floor… and props on the fourth… and props on the third… second… first…

Ouch! Is it by mistake or design? Turns out this state-to-props issue is by design, and this is where Redux steals the show. However, five minutes into the Redux tutorial you begin to realize that your excitement about React was an overreaction… or was it?

There were many attempts I know of to steal the show back for React, to name a few: React Context API as well as various state managers; but for me, none of them got the excitement I initially had for React back… except when I learned about RxJS.

Analysis

This article’s main goal is to justify that the RxJS and React combo allows for better readability, less boilerplate and being closer to React while allowing the same functionality as the popular state management frameworks and not being a framework itself. Let’s look at this approach more closely.

Better readability & less boilerplate

Artwork depicting code mockup that the developer who wrote it doesn't recognize
We’ve all had these moments

Readability is one of the first things I pay attention to as I choose a tool I am going to be working with. For me, the main criterion upon which I evaluate the quality of my own code is how well am I able to understand it a week after and a month after it was written.

Of course, the tool you are using isn’t always as impacting on your code readability and self-documentability as much as your own set of habits and choices, but it still may be a contributing factor nonetheless. In my experience, the more verbosity and indirection is associated with creating a specific functionality, the less you will be able to understand it later on.

And this is true of Redux. Before even getting to the core of what it is you are doing, you have to write your actions (some people write action creators on top of that), create reducers, compose them, connect dispatch and state to props, and only then introduce the logic, and don’t forget you have to write sagas or thunks for side effects.

When you are trying to understand the structure of a Redux application, you are facing similar challenges, you have to understand what actions trigger what side effects, what happens in reducers and how it all fits together. I’ve always considered it being a nightmare, and, despite my many attempts, never really understood my colleagues who were very passionate about Redux. I would say Redux and readability are essentially the opposites. Feel free to disagree, but please make an educated argument in the comments for me and other people to be able to learn.

MobX significantly improves the readability aspect by not having the verbosity and indirection of Redux. Yet it can be confusing to mentally derive the structure of the state that consists of composed stores, as they can become a mess of properties and update logic. So how does it compare to the RxJS Hook?

Since you’re essentially using RxJS Hook as the familiar useState hook, it should be very easy to read and understand. Your state lives inside of a Subject that you can always retrieve later and analyze as you are trying to understand what the application is doing.

Close to React

artwork depicting various parts of the React ecosystem
It’s quite an atomic family

React is elegant. The state managers are not. Period. If there is an opportunity to stay with React before introducing other frameworks, I will use that opportunity. I am convinced that you should be able to compose complex and elegant application code with essentially React and Services logic while not sacrificing readability, maintainability, and scalability.

RxJS and React Hooks are actually the combination I find extremely helpful in this regard. Having actually written production code with this, I can tell you from my experience that it was and still is a great ride.

State Management Functionality

artwork depicting various stages of an atom
Creation of the modern front-end universe

I never really understood the popularity of Redux. Don’t get me wrong, I always thought it was pretty interesting, but I never liked writing my code using it. I wonder if its popularity really comes from the fact that people like Dan Abramov, who is a great guy and a great engineer, but it doesn’t make sense why it would still worth biting the bullet.

The main merit of Redux for me is in its three principles of course, which are:

  • Single source of truth
  • State is read-only
  • Changes are made by pure functions

So none of these principles actually work for MobX! With MobX you don’t have a centralized state object. You have a centralized store object. Know the difference. 😉
And since you don’t have a centralized state object, you can’t use it to easily reason about your application as your stores and your state are interleaved and can be pretty hard to unravel.

With RxJS Subjects and React Hooks we get the one principle out of the box – the pure-functions, as observable streams are themselves pure-functions. Unfortunately, the state of a BehaviorSubject is not read-only, and you can manually change it after you get it with getValue(). I will try to ask the community if it is possible to make it read-only out of the box so that you can’t shoot yourself in the foot with this, as I am pretty sure it could just be something the contributors have overlooked. But as to the single source of truth, it is actually not that difficult to introduce by composing all the subjects into one observable, similar to how you compose reducers together.

Single Source of Truth

const subject001 = new BehaviorSubject({});
const subject002 = new BehaviorSubject({});
const subject003 = new BehaviorSubject({});

// Only for listening
const globalObservable = combineLatest(subject001, subject002, subject003);

In general, the RxJS Hook approach is not that different from Flux. You have:

  • Dispatcher/action – subject.next() / setSharedState()
  • Store – globalObservable
  • View – well, your view

TL;DR

So here is your shared state!

const subject = new BehaviorSubject({ message: '' });

const AwesomeComponent = () => {
 const [{ message }, setState] = useSharedState(subject); // Custom Hook
 return <div>{message}</div>;
};

Nothing unusual: we are getting a tuple of value and setState function.

Do you want to use it like useState?

Easy!

const AwesomeComponent = () => {
 const [{ message }, setState] = useSharedState(subject);
 return <div
   onClick={() => setState({ message: 'test' })} // One way
   onClick={() => subject.next({ message: 'test' })} // Another way
 >{message}</div>;
};

subject.next is another way to update the logic, but I think setState from the tuple makes it more readable and easy to understand.

Do you want it to be shared between components at the top of the tree as well as the bottom?

Easy!

const First = () => {
 const [{ message }, setState] = useSharedState(subject);
 return <div>First: {message}</div>
}
const Second = () => <div><First/></div>
const Third = () => <div><Second/></div>
const Fourth = () => <div><Third/></div>

const Fifth = () => {
 const [{ message }, setState] = useSharedState(subject);
 return <div><div>Fifth: {message}<div><Fourth/></div>
}

Notice that we can use the same state easily, where in the past you would have to pass everything through props.

Do you want to use both local and shared states?

Easy!

// Single shared and local states
const AwesomeComponent = () => {
 const [sharedState, setSharedState] = useSharedState(subject);
 const [state, setState] = useState(‘’);
 return <div>{/* That was easy */}</div>;
};

// Multiple shared and local states
const AwesomeComponent = () => {
 const [sharedState001, setSharedState001] = useSharedState(subject001);
 const [sharedState002, setSharedState002] = useSharedState(subject002);
 const [sharedState003, setSharedState003] = useSharedState(subject003);
 const [state001, setState001] = useState('test');
 const [state002, setState002] = useState('test');
 const [state003, setState003] = useState('test');
 return <div>{/* That was easy */}</div>;
};

Another great thing is that you actually can have multiple little states as part of your component state.

Oh, but what if I want to update this state outside the component…

Easy!

subject.next({ message: 'I know Kung Fu' });
artwork depicting Neo from the Matrix movie
Or so the course certificate says

You can update state from anywhere in the application, by just using subject.next. It is extremely useful! MobX and Redux could never give you this much freedom.

Still not convinced?

I rewrote the Redux tutorial using the RxJS Hooks approach. Feel free to compare two codebases that do the same thing. Here‘s the original Redux Reddit Tutorial and here‘s the React + RxJS Reddit Tutorial.

Side Notes

I think somebody brought to my attention that using components with subjects makes them tightly coupled. It is true in a sense where, for example, you are importing something from an external library which makes a hard dependency. But having hard dependencies doesn’t mean you can’t make whatever it is you are building highly reusable. A styled component inside of my custom component is a hard dependency, but it is abstracted out when I import my custom component. The same goes for this approach.

Conclusion

Behind the custom hook is a little pretty eight-liner:

const useSharedState = subject => {
 const [value, setState] = useState(subject.getValue());
 useEffect(() => {
   const sub = subject.pipe(skip(1)).subscribe(s => setState(s));
   return () => sub.unsubscribe();
 });
 const newSetState = state => subject.next(state);
 return [value, newSetState];
};
artwork depicting Neo from the Matrix movie
Just an ordinary day of a React developer

Is this really new?

Not really, there are many blog posts about similar concepts, but in my opinion, people always slide into the complexity trap and start to introduce new frameworks.

This article is almost a mirror of this one. I didn’t know there was an article about essentially the same thing. I assure you I came up with this independently. But this doesn’t really matter. What matters is to be faithful to React’s promise: React makes it painless to create interactive UIs. and try to avoid the unnecessary complexity of introducing a new state manager or a framework, this is so besides the point! (Sorry Akita, but you fell into that trap)

9 Replies to “React Hooks + RxJS or How React Is Meant to Be”

  1. I like the solution but IHMO the approach is quite different compared to Redux and it cannot replace Redux.

    With Redux, the state only can be changed using actions and reducers. This allows you to control what changes are made, you can keep a history of every change and going back to any previous state (by just keeping a list of actions, you don’t need to copy the whole state on every change).

    Redux uses HOCs. While testing, you can use the component without the HOC and can easily mock everything that is passed into the component by Redux. How could this be done using with your approach and hooks? You can mock modules with Jest but I never found an approach that works really well with TypeScript and without duplicating a lot of code.

    1. Kirill Novik 5 years ago

      First off, thanks a lot for your comment!

      I actually would like to challenge your point about the state modification. BehaviorSubject is more like an action/reducer combo, not the whole state. Remeber, that I argue in the article that we use combineLatest to get the whole state.

      To enable time traveling, we could use either ReplaySubject that can keep a reference of BehaviorSubject and a value the subject is being assigned, so it is essentially the same functionality as Redux and just a couple of lines of code.

      You can write your own HOCs too. I personally don’t mock my shared state, I just set it the way I want.

      Not sure I understand the problem you’re having with TypeScript, though.

      I really would encourage everyone looking at this to try this approach and to write a small app to really get to feel it. Here is a starter that you might have already looked at: https://codesandbox.io/s/how-react-should-be-50wp0

  2. Interested Party 5 years ago

    I’m not familiar with RxJS, how does this fare for performance? One of the key features of the react redux combination is in the react-redux connect method preventing unnecessary renders based on the return value of mapStateToProps.

    Would this approach re-render all components connected to state whenever any piece of the state changes?

    1. Kirill Novik 5 years ago

      Great point!
      So, technically I would use either of these approaches to prevent unnecessary rerenders:
      1. Split state in separate BehaviorSubjects
      or
      2. Pipe your subject through filter like:
      useSharedState(subject.pipe(filter(s => s.type === ‘test’))) // Let’s say your state looks like { type: ‘type’, content: ‘content’ }

  3. dotintegral 5 years ago

    I really like RxJS and would love to use is more often than I do. But, I have worked in a react project that had its state management replaced with stream library. Though initially it was a neat idea and made some sense to me, quickly I realised that it’s not something I would recommend anytime soon.

    The main con of this setup was the ability os seeing what’s going on in the application. Though Redux is pretty simple, everything happens as a result of actions. I agree, that sometimes tracking the origins of an action in middleware might be difficult. On the other side it’s totally transparent, by that I mean that it’s easy to see that changes to the global state were made because of a given action. I can easily look through all fired actions because there are some great tools, like the simple redux-logger or browser extensions.

    The RxJS has a great problem – it’s hard to debug. There are no really good tools to look inside the streams and figure out what’s happening. Most of the times I found myself just writting ‘tap’ functions and setting breakpoints where I thought that something might be going wrong. This, combined with the async nature of RxJS, makes debugging really painful process. It might not be easy to see when working with simple application that does not requires complicated streams. But with more complicated cases, as in most commercial project, streams are getting more complicated as well. And debugging them becomes an issue.

    This is why I don’t recommend this approach for commercial projects.

    1. Kirill Novik 5 years ago

      I agree, it can be very difficult to debug. But the beauty of rxjs is that it gives you choices. Instead of composing streams you can turn your observable into a promise and do the imperative programming that is easier to debug.

  4. Tony Brown 5 years ago

    Just what I’ve been looking for.
    I was thinking to myself, that it would be a great idea to use RxJS’s observable streams with React

  5. To have any Subject and subtypes (Behaviour, Replay…) as read-only you just need to encapsulate the Subject and only expose it as .asObservable().

    private _bearsStream = new BehaviourSubject(new Bear()); // use this internally to the class.
    public bearsStream = _bearsStream.asObservable();

Leave a Reply