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.
looked at how one could implement a Redux like store using a few lines of code
and the new React APIs
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
equivalent hook. Wrapping such a component in
React.memo would not help as we
are rerendering it from inside by using
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
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.— Sebastian Markbåge (@sebmarkbage) 4 January 2019
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.
- 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
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.
useReducerhook 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.