Are new React APIs replacing Redux? (Part 3)

Disclaimer: this post series is discussing alpha features of React. A lot of
this is up in the air and is being actively discussed by the React team and the
community. The features, problems, solutions, opinions and decisions that these
posts discuss might not be final. This is as a summary of the trade-offs being
discovered and discussed. I find these behind the scenes discussions to be
interesting and educational. Writing these posts is my way of understanding the
topics in more depth. Finally, the content is interleaved with with my personal
experience and opinions. I hope you enjoy!

This is Part 3 of a series of posts taking a look at recent React APIs — Context
and Hooks to see if they are a good replacement for Redux.

In Part
1,
we looked at how React Hooks will most likely simplify how we use a lot of our
existing tools, such as state management libraries. For example, Redux, Apollo
and other library bindings will likely become Hooks as they are simple to use
and are a powerful abstraction. I also claimed that if you’re using Redux, you
should continue doing so, because it does quite a bit under the hood to make
things convenient and optimized for performance.

In Part
2, we
looked at how one could implement a Redux like store using a few lines of code
and the new React APIs useContext and useReducer . We then looked at why
this approach has drawbacks — namely your app might get rerendered too much on
each state change. That happens because of unrelated state changes and because
of lack of batching. These drawbacks can be solved by a library, such as Redux.
Avoiding performance pitfalls is why you should consider using a battle tested
library instead of rolling your own.

In this post I will revisit parts of the
facebook#14110 GitHub issue
which had more activity since the first post in this series was published. I
will summarise recent changes to Redux and how it has run into problems
utilising React Hooks. I will also briefly discuss how and why the React team
might be questioning and challenging the use of Redux.

Version 6 of react-redux library has been released recently (I will refer to it
as Redux going forward). This version of Redux started utilising the new React
features. In particular the custom subscription mechanism has been removed
entirely in favor of the React’s Context API. Previously, Redux used the legacy
Context API to only propagate the store instance, but was using it’s own
subscription mechanism to listen to store changes. In the new version, Redux
uses the new Context API . Redux now uses context to propagate the store state
(as opposed to store instance) to all connected components, but also to react to
state changes without the need for a custom subscription mechanism. This has
been done to reduce the size and complexity of the codebase by leveraging
React’s own functionality. This is also meant to prepare Redux to work with the
React’s upcoming Concurrent Mode.

Unfortunately, switching to the Context subscription approach means that Redux
can not implement Hooks for binding components to the store (at least at the
time of writing this post). This is because Context holds a single value, in
this case the entire state of a Redux store. Any change to any part of the state
causes every Context subscriber to be notified of the change. This is not a
problem if using the connect Higher Order Component, because it utilises
shouldComponentUpdate (or equivalent) to prevent the subtree from rerendering
if the mapped state hasn’t changed.

But when using Hooks inside the render function, for example:

const user =useRedux(state => state.user)

the “connected” component is already in the process of being rerendered and so rendering of the
subtree is unavoidable. In other words there is no shouldComponentUpdate
equivalent hook. Wrapping such a component in React.memo would not help as we
are rerendering it from inside by using useContext.

Note that it is possible to implement Hooks based bindings for a store like
Redux. At the moment, however, that means that the library would need to use
it’s own subscription mechanism. Instead of using useContext to rerender on
any store change, the library would subscribe with a custom subscription
mechanism in a useEffect hook and use useState hook to only update the local
component state when the relevant part of the store changes. You can see an
example of this in this GitHub
comment.
Even when using this approach, you need to be careful to avoid unecessary
rerenders as discussed in Part 2 of this series.

In general, when writing your own “simple store” library instead of using a
battle tested library you might run into issues like too many rerenders, or out
of order renders. Having said that, do not be discouraged to experiment with
this stuff! Reinventing things is a great learning experience and you might
crack some of the API design challenges and come up with good suggestions for
the React team. Just be aware of the issues discussed in this post series when
shipping such code to production.

As Sebastian Markbåge summarised in this
comment,
using Context to subscribe to state changes is probably the right direction
going forward, but might be too early given the constraints of the current API.
Other store implementations that are using subscriptions can migrate to using
Hooks, but will probably not be Concurrent Mode compatible.

Personally, I’ve been enjoying Hooks so much that I’ve taken the leap and
implemented bindings using Hooks in a store
library I maintain. The Hooks make the
API simple and nice to use. But as Seb pointed out, my library is probably not
Concurrent Mode compatible. Given that both the Hooks and Concurrent Mode APIs
are not finalised and might be missing some expressivity (it’s a really hard
problem!), I’ll just have to wait this out and see what solutions and guidance
emerges from the React team.

So far we have been discussing how to utilise the new React APIs to implement
global Redux style stores and the pitfalls to watch out for. But is a global
store a good way of modeling applications? I’ve been noticing that some of the
React team have been discouraging the use of global stores. From my personal
experience, discovering the flux (unidirectional data flow) and then Redux (same
but with a single store) approach was a breath of fresh air coming from Backbone
model based applications. In my experience, working with Backbone models was a
bit rough, you never exactly knew what state your application is, it was
difficult to debug and the code was very imperative with a lot of manual
subscribing, unsubscribing, sets and gets. Many people have since moved to use
the single store approach via Redux or equivalent libraries. This happend in non
React communities as well (see Vuex store for Vue).
But maybe there is an even better approach? (Spoiler: I don’t know).

The message from the React team or other Facebook teams, such as Reason React
team has been — “do not use global stores” (at least that’s the message I’m
perceiving). I wonder if that is because such solutions simply don’t scale.
Facebook is a big company with many teams and large applications. I understand
how for them it’s impractical to keep all the state in one global store. In
addition, React team has been working on Concurrent Mode which can render your
application in multiple asynchronous passes to improve user experience.
Concurrent mode can pause and reprioritise parts of the rendering to avoid any
user noticable jank for high priority parts of the app. Reconciling global store
with asynchronous rendering of render subtrees is challenging. You don’t want to
end up with tearing where parts of your application get rendered against
inconsistent snapshots of your state. Reconciling central stores with concurrent
mode is something that React team is working to
solve.

Finally, here’s the most recent comment on the global store topic from
Sebastian:

React local state is like Rust. You have to think about the owner so you know how long it will live. (Most) Flux is like arena allocation. It just keeps growing indefinitely until the user reloads or the tab crashes.

This made me further question the global store approach, which I’ve been
personally really enjoying working with. The more global state you add, the more
global actions you add, the more likely you are to introduce bugs due to store
being in an unexpected state, the more likely the code could start turning into
spaghetti, the more likely you are to run into memory lifecycle management
issues.

Having said that, you should use what works for you! For now, I’m sticking with
the global stores. I think of a store as a client side database of the
application. Easy to connect and project into any part of the app and
predictable to update by dispatching one of the predefined actions. But I will
also continue exploring alternative solutions to this problem.

In conclusion:

If you’re using Redux, continue doing so, it will continue evolving to use the
best practises and will gain support for new React APIs over time.

Redux currently can not offer Hooks based bindings, but that will be solved
either by introducing some new React API, by reverting Redux to use
subscriptions or by some other approach.

Today, React Context is good for low frequency unlikely updates (e.g. locale,
theme), but it’s not ready to be used for Flux style state propagation. (See
comment by Sebastian
Markbåge).

Be careful if you’re rolling your own store implementations based on
useContext, useReducer, useEffect, useState— there are a few pitfalls
to watch out for (Redux handles more complexity for
you
than you might think).

Prediction: entirely new approaches to state management might emerge in the
upcoming year (profunctor
optics!?). There already exist
popular libraries such as MobX and Apollo that prefer a local state approach.
The useReducer hook and React Suspense might rise in popularity or might be
wrapped into an entirely new library for state management, where immutability,
unidirectional data flow, locality and convenience all get combined in a better
way.

Thanks for reading! This was tricky to write due to the fact that these topics
are a bit in flux (😏) and are being actively discussed and designed. In any
case, I hope you’ve learned something new. If there’s anything I should correct,
improve or clarify, let me know on Twitter or
in the comments.

Read Next

I’ve been looking for an alternative to Google Analytics for a whilte. Either a beautiful, simple, easy to use Open Source project or a reasonably priced paid service. I haven’t found any decent paid services priced at around $ 1 per site per month. But I have found a couple… Continue →