You Might Need Redux

Published: October 16, 2020

When my kids were babies, sometimes it felt like a one-hour trip to the store required more planning than an expedition to Antarctica. We had a diaper bag we packed with all the things we thought we might need, but it was the way it was packed that mattered. Diapers and wet bags and wipes have to be accessible so they need to go at the top. So does a bottle of milk and a spit rag. And a change of clothes ... sonner or later you realize: everything has to go at the top.

I recently thought about this while adding application menus to an existing React app that does not use Redux. Menus can do anything - they are concerned with all manner of state and all manner of components in the applications. Since a core React principle is that state must live at a level higher in the component tree than the DOM elements that depend on it, virtually all the state in the application had to be pushed up at least to the level of the menus (which were near the very top of the tree). Everything has to go at the top.

I think this illustrates a general problem with using React to manage application state. It isn't a problem for trivial data, for example the string of text in an input field (although even that can be non-trivial). But as soon as state becomes concerned with multiple parts of the DOM, it often has to live many levels above the components where it is actually controlled and/or displayed. The outcome of this is that components, which ideally are engineered in isolation, need to be engineered in the context of both their parents and children. Code is harder to reuse, bits of logic get duplicated, and bugs are harder to trace because the state of the application at any one time is never clear. Eric Elliot called this last complexity "time-travelling spaghetti".

I am not arguing that you have to use Redux itself. But, if you accept that all your state is going to live at or near the top of your component tree, you better have a good strategy to manage it all. useState is not good enough; as soon as anything becomes sufficiently complex it's a tangled mess. useReducer seems like a simpler of version of Redux itself, which might be fine. Of course we're going to need a way to manage network requests, and we might want middleware, and wouldn't it be nice to have a debugging tool that logs actions -- and oh boy we just reinvented Redux.

The best criticism I've heard of Redux is that it makes simple things hard. This is undeniably true. If you want to manage a checkbox in Redux, you need an action creator, a clause in your reducer, and you need to write the binding to the component with the checkbox. To truly use best practices, you also need a selector that picks the checkbox state out of the store. What an immense amount of work to flip a checkbox from off to on. Finally, you have to contend with JavaScript's original sin: using inherently mutable data types (objects are arrays) and ensuring that they never mutate.

Another frequent pain-point for new adopters is that Redux is barely useful on its own. To use it for virtually any frontend application beyond a demo, it also requires on to pick middleware for managing asynchronous actions.

While these are completely valid criticisms, and the Redux maintainers would do well to listen to these voices (and they do seem to be) it is critical to note that the tradeoff to making simple things hard is making complex things manageable. The advantage of having a defined space of all possible application states is that is that all possible states can be reasoned about. In general, if a given state is possible based on the defined shape of your state, it should produce a defined result in the DOM (one that doesn't result in fatal errors or cause the app to look broken). If a state is possible, it should be handled, even if the path needed to get to that state is not clear to the programmer.

This is especially true when one considers that the possible edges are impacted not only by user behaviour but also by unknowns in the network conditions and the server.

Application state can be decoupled from the view. This it makes it easy to shift between states if you need to add menus or key commands, and easy to write simple, predictable unit tests. Optimizing re-rendering becomes much easier when you can see determine exactly which state transitions will affect the component giving you problems.

Frontend development is currently contending with two crises simultaneously: the bewilderingly rapid increase in the complexity of our toolkit, and facing up to the fact that much of the software we write is brittle, bloated, underperformant, and broken. Some have even argued that single-page applications are too hard for anyone but the richest software companies to invest in.

Often, these two crises are conflated and discussed in the same breath. But we need to recognize that tools can both increase the amount that a developer needs to know, and decrease the complexity of an application at scale. Redux is clearly and example of this, and the field should be careful to move past it and technology like it.