Recently, Redux maintainer Mark Erikson, brave soul that he is, posted the following to Twitter:
When interviewing React devs and talking state management the most frequent responses I get are:
- I use Redux and Redux Thunk for state management, although I now use hooks or Redux Toolkit which have made Redux better
- I don't use Redux any more, because the Context API covers my use cases
When I ask about problems with Redux, I tend to get the same answer Mark stated: "Too much boilerplate"
The boilerplate referred to is the view that a single concern in Redux seemed to require 3 files:
- a file for your reducer
- a file for your action type constants
- a file for your action creators
Multiply by the number of concerns in a non-trivial app, and you end up with a lot of files. Dan Abramov structured his early Redux examples like that and it became de rigeur to follow suit. While he did it only to separate the concepts he introduced, the structure took hold (along with some other unhappy stylistic choices) despite it being easy to express things differently.
That bred the pat answer that boilerplate is the problem with Redux, with the implication that if you fix this, it's all fine.
I don't agree. I don't mind the boilerplate. For one thing, a bit of extra ceremony is good if it helps readability and the boilerplate has no bearing on why I won't ever choose to use Redux again.
Asynchronous effects were always an afterthought in Redux. The original examples were about simplifying and popularising the flux architecture and demonstrating time travel debugging. By only considering synchronous state, only half the problem was addressed.
An array of async middleware tried to bridge the gap: thunks (more on those in a bit), sagas (bringing a heavy cognitive load from generators), promises (which might have become the standard async pattern had the author's monicker been "gaearon" and not "acdlite"), loops (a botched implementation of elm's effect model), observables (FP streams, the Redux way) and now we have Redux Toolkit's asyncthunks. I even offered my own take, using custom middleware to express asynchronous Redux in an idiomatic manner: https://medium.com/@christianchown/dont-use-async-redux-middleware-c8a1015299ec
The fact that 5 years on from invention, the community has not coalesced around a happy async pattern is indicative that expressing asynchronous actors is not something that comes natural to Redux, despite it being crucial to application function.
The fundamental Redux contract is the flux one: actions flow around your app one way. Your app is in a specific state, an action flows through it and you get a new state. Reducers can act on actions they don't define - the Redux docs talk about how useful it is that a
LOGOUT action might reset multiple slices of state.
Thunks break that contract. You dispatch a thunk action, it flows into that thunk function and never gets passed on. Should a different reducer or different thunk want to receive it, well, tough.
Redux Toolkit seems to acknowledge this ugly effect by now spitting out extra pending/fulfilled/rejected actions for thunks, but a reducer reliant on a previously sync action turned into a thunk must now be refactored for these new actions. You may well not know about, or even own, that other code and you just broke it. Brittleness is baked into Redux.
Thunks were only meant as stopgap until the community came up with something better. It never did, and now Redux Toolkit codifies them as being best practise.
It's clear in Redux where the source of the application state is, the store. Less clear is where the effect logic should be located and that's because, by design, it's imperative and it's scattered through a codebase. Some of the effects are in reducers, some is in async middleware; some invariably ends up in consumer components. This confusion makes it more difficult to tell why a particular mutation occurred.
The problem is especially prevalent in saga-based systems. Sagas improve on thunks in two ways: they consolidate the location of application logic and don't suffer from the function-that-swallows-action anti-flux problem, but at scale, it can be hellish telling why a particular action fired. Each saga is an invisible source of state: at what step of the generator is each saga in? Your Redux store may be in a single, well-defined state, but the same action in your Redux system can produce different results because the generators are at different points. Using thunks doesn't solve this; a non-trivial thunk is stateful too.
I recently put together an xstate state machine that I envisaged being a single entity. During development, I realised a whole chunk of its functionality could be broken out, both simplifying the system and yielding a new and useful reusable element.
I have never once achieved this with Redux logic. It's not part of the Redux architecture or mindset that you compose slices out of smaller slices, or break out a section of a slice for use elsewhere, yet this is a really powerful means for creating better software.
It's also fun. Making something simpler, smaller and better while you're creating it is really rewarding as a developer. Working with Redux is not fun.
The atomic state solutions (recoil and jotai) lend themselves to this, deriving atoms from atoms. Context solutions wholly avoid it by strictly separating concerns.
Redux applications end up as monolithic messes. Certainly when they reach a jumbled situation, there are better ways in which they could be organised, but by then, the tech debt has already been accrued. Continuous improvement throughout development is not easy to achieve in Redux.
There are other shortcomings too. David K Piano points out that action effects in Redux are not declarative. There are also other useful application primitives such as delays or long running activities that are not natively expressable in Redux.
Redux has given a lot to state management and to React in particular. For me, it solidified and exemplified concepts I use every day: the flux architecture, immutability, pure functions, reducers and more.
But persisting with it when there are better choices is leading to lower quality software. Mark Erikson is an incredible advocate for Redux and it's lucky to have him. My worry is that keeping it alive past the point of its usefulness is stunting the growth of superior alternatives and is damaging to the React ecosystem.
I don't doubt he'll read this and will eloquently disagree, but there are lots of absolutely godawful Redux applications being created out there, and some of the blame of that lies not with the skill of the developers, but with the library itself.