Last month, we announced the release of the new website of React-RxJS, our React bindings for RxJS. If you’ve ever had to integrate real-time data APIs with React, keep reading, this is the solution many of us have been waiting for.
In this blog post, we will explain why we needed to bring reactivity to React through RxJS, and our thoughts on why -for the real-time data applications that we build at Adaptive- we can’t just use React as a state-management library. At least, not directly.
I'll be looking forward to hearing your thoughts/feedback, find my contact details at the end of this article.
Since it was open-sourced in 2013, React has become one of the most popular tools for building web applications. At Adaptive, we quickly realized its potential, and we became one of its early adopters. However, React’s API was still a bit rough around the edges, and it was not ready for handling domain state. As a result, different libraries were created to cover that gap. Redux became the most popular one, and a large ecosystem emerged around it.
The Redux value proposition was very appealing. It proposed a simple mental model that seemingly provided code-consistency, maintainability, predictability and great development tools. At Adaptive, we adopted Redux, and we accepted its shortcomings as necessary trade-offs.
However, React has improved a lot since then: a stable context API, React Fiber, Fragments, Error Boundaries, Hooks, Suspense… And there is another set of great improvements that are about to land with React Concurrent Mode.
All these improvements make Redux obsolete. On the one hand, React now has a much better API for dealing with domain state (mainly thanks to Hooks and Context), on the other hand, Redux has now become an obstacle for leveraging some of the latest React improvements.
React’s state-management is not reactive, though, and that can be a challenge when it comes to integrating real-time data APIs with React. However, due to the latest React improvements, it’s now possible to have a set of bindings that seemingly integrate Observables with React, and that is exactly what React-RxJS is about. React-RxJS goal is to bring reactivity to React. Let’s see why this is highly desirable for real-time data Web Applications.
Why did we start using Redux?
Before we explain why we have decided to stop using Redux, we must understand why we started using it in the first place.
React shipped its first stable Context API on version 16.3.0, which means that for the first 5 years React didn’t have a stable API for sharing state. In fact, during the early years, React was presented as a tool for enabling Flux. During that time, Redux became one of the most popular tools for managing the state of React applications. Redux was so predominant that even React-Apollo used it internally on its first stable version.
Probably what enabled Redux popularity was its unopinionated API, which allows to easily enhance the Redux store. In other words: Redux popularity was enabled by its middlewares. Even the Redux devtools are a store enhancer! Thanks to middlewares like Redux-Saga and Redux-Observable, many of us saw in Redux not only a library to handle state, but a means for orchestrating side-effects.
At Adaptive, we specialize in real-time data applications, and most of our APIs are push-based. Therefore, Observables are a central primitive for us. So much so, that you could say that Reactive Extensions are a lingua franca inside Adaptive.
When React came out, it was very challenging to integrate RxJS directly with React. React was essentially pull-based, and that presented a significant impedance mismatch with RxJS observables, which are push-based. In that context, Redux-Observable looked like the right tool to integrate our APIs with the Redux store that fed the state of a React App.
However, after having used this tech-stack for the last years, we’ve learned that it can have a significant impact on performance, scalability and maintainability for the kind of Web Applications that we build.
Why did we decide to stop using Redux?
Some Web Applications have the luxury of interacting with APIs that spoon feed them with the exact data that they need for a particular view, like GraphQL APIs. Those kinds of APIs have many advantages, but they require some extra-processing and caching on the Back End, and they tend to produce relatively large payloads. Unfortunately, for most of the products that we build, we can’t afford the luxury of working with those kinds of APIs. Our APIs send frequent updates with small payloads, and most of these payloads consist of deltas. In other words: in order to keep our BE services highly efficient, the client is expected to reactively derive a lot of state.
Redux is not ideal for this, mainly because it’s not reactive. Redux treats our state as a blackbox, without any understanding of its relations. We can “slice” our reducers as much as we want, but all that Redux sees is one opaque reducer. Also, it often happens that after we’ve broken down our reducers into small slices, we run into a situation where the reducer from one slice depends on the value of another slice. There are different “solutions'' for addressing this common problem, of course. However, they are all hacky, suboptimal and not very modular.
Ultimately, the problem is that since Redux doesn’t understand the hierarchy of our state, it can’t help us at optimally propagating changes. Every time an action gets dispatched, Redux will try to notify all the subscribers. However, it often happens that while the store has started notifying its subscribers, one of them dispatches a new action and that forces Redux to restart the notification process. In fact, the subscriber doesn’t even know if the part of the state that they are interested in still exists. That’s why react-redux has to find creative ways to work around problems like “state props” and “zombie children”.
The fact that Redux “chaotically” notifies all the subscribers upon dispatch is problematic. However, there is yet a larger issue: to prevent unnecessary re-renders, all subscribers must evaluate a selector function and compare the resulting value with the previous computation. If this selector just reads a property of the state, then things work fine. However, applications that derive and denormalize significant amounts of data must use tools that help at memoizing selectors, so that they can avoid unnecessary recomputations and unwanted re-renders. However, these tools are quite limited and inefficient.
Another important problem when working with Redux is code navigability. This can be especially problematic when using Redux-Observable, because it’s very tempting to make transformations in the epics and let reducers become glorified setters, with actions that read like “SET_SOME_VALUE”. When this happens, then understanding what’s dispatching those actions and why becomes really challenging as the project grows.
Other issues when working with Redux are that it makes code-splitting a very tedious endeavour, it doesn’t provide any means for integrating data-fetching with React.Suspense and the support that provides with React’s error boundaries is quite limited. Also, it’s quite likely that when React Concurrent Mode gets released, react-redux will have to choose between suffering from tearing issues or having to pay a toll on performance.
Why not use React as a state-management library?
React has improved a great deal during these last years. However, React still treats its state as a black box. It doesn’t understand its relations. In other words: React is not Reactive.
Generally speaking, that’s not a problem when building reusable components. However, when it comes to building components that are tightly coupled to the domain state, especially when this state is exposed through a push API, then using React as your state management library may not be ideal. Observables would be a much better fit for that.Wouldn’t it be nice to have a way to integrate those domain observables with React easily? Well, that is exactly what React-RxJS accomplishes. React-RxJS leverages the latest improvements of React so that we can easily integrate observables that contain domain state with React. Doing so has the following benefits:
- Updates are only propagated to those observables that care about the update, so we are automatically avoiding unnecessary recomputations and re-renders without having to memoize selectors.
- Since we don’t have a central store or a central dispatcher, we get code-splitting out of the box.
- Much better code navigability, as we can easily navigate the chain of Observables that define a particular piece of state.
- Much less boilerplate. Also, since the hooks produced by react-rxjs are automatically integrated with React Suspense and error-boundaries, we can get rid of all the ceremonies that are needed with Redux for dealing with loading states and error propagation. Therefore, producing code that’s a lot more declarative, while also producing smaller bundle-sizes.
At Adaptive, we have been using these bindings for the last few months in production, and based on the performance gains that we are experiencing and on the reduction of boilerplate that React-RxJS has enabled, we can confidently recommend its usage. Also, one nice thing to be aware of about these bindings is that since React-RxJS doesn’t want to own your whole state, it can easily integrate into your current project and grow organically. This is particularly relevant for those React projects that were started years ago with Redux and Redux-Observable.
React-RxJS makes React Reactive. In the sense that enables handling the domain-level state of a React app using RxJS streams. So, if you are looking for a modular, performant and scalable state-management solution for React, especially if you are working with push-based APIs, then you should give it a shot.
Victor Oliva: co-creator of these bindings, he has helped at shaping the API, fixing bugs, coming up with great ideas, improving the documentation, etc.
Bhavesh Desai: for believing in this idea since the very beginning. He was the first one who thought that we should try using RxJS directly with React and he promoted the first ideas and experiments.
Riko Eksteen: for his invaluable help at improving the docs, providing feedback on the API, improving the typings and the CI, and for always being there ready to help.
Ed Clayforth-Carr: for coming up with this awesome logo.
Josep M. Sobrepere
Front End Architect,
Adaptive Financial Consulting