redux-thunk
is bad architecture for organizations
Don’t use redux-thunk, especially in the 2020s
When starting with React, many people being using redux with redux-thunk. After first seeing it many years ago, I quickly smelled many anti-patterns and tried to move teams off of it. Nowadays, with Apollo GraphQL, React Context, and React Hooks among other features, there’s less reason to use it anyways.
The anti-patterns became much more apparent when working on larger teams. It doesn’t matter which frameworks you use on your personal TODO app, but when it comes to organizations, frameworks matters a lot. As a corollary to Conway’s Law, poorly architected code can lead to poorly architected organizations and vice versa; frameworks help decide your architecture. I’ve seen the effects of poorly architected code first-hand with redux-thunk.
Middleware is a smell
Not all middleware is bad, especially if it’s logic that must be executed with every function call. However, it is a layer of abstraction that makes your code more difficult to understand. As a maintainer of Koa and previously Express, I felt the pain of middleware — engineers requested various middleware that handle everything for them. For example, engineers may think they want middleware to access req.currentUser
on every request. However, instead of middleware, what engineers really want is a method like const currentUser = await req.getCurrentUser()
which retrieves the current user only when requested. Unfortunately, for most pre-async function, callback-based frameworks like Express, this would’ve been a pain.
In fact, this has been my perspective on the complexity of the JavaScript ecosystem: so many frameworks, so many opinions, and so many libraries were created because people found JavaScript’s asynchrony complex and tedious. People’s pain with callbacks motivated them to create frameworks to halve the number of lines of codes they had to write. Most of these solutions involved “middleware”, but with the advent of async functions, there is less need for middleware.
Middleware should be using sparingly and generally for two reasons: logging and debugging. Middleware should generally not be used for business logic; any middleware that adds business logic to your system will make your system inflexible and obtuse. Although redux-thunk middleware does not contain business logic, it pushes business logic execution to the middleware, hidden from engineers who don’t know what they’re looking for. As an organization matures, this has many effects such as a decreasing velocity, a decreasing inability to innovate, and an increasing propensity to solve problems through poorly architected microservices just to escape their current trap of poorly architected middleware.
getState() obscures data’s and action’s dependencies
As mentioned in the Idiomatic Redux Series, getState()
breaks one-way data flow. A more practical perspective of this anti-pattern is that it obscures what the data flow is—it obscures how data and actions are dependent on each other by creating implicit dependencies on the current state. Writing logic with getState()
will be very easy at the beginning, but once you try to untangle and understand your thunks, especially when migrating to a new framework, you’ll realize that you just created a ball of mud.
This dependency spaghetti will make migration and innovation more difficult. You may try to migrate a page to use Apollo GraphQL using React Hooks, but you won’t realize that another page relies on data generated from your thunks until someone reports that broken page. This is especially true when your organization also has messy team dependencies where all the teams are working on the same repository without any clear delineation of ownership, which I’ve never found to be not the case. Organizational dependencies are difficult problems to solve, but code dependency problems are entirely under engineering’s control.
thunks can cause infinite recursions
Perhaps the most painful experience with redux-thunk is when it causes an infinite recursion. Because data and action dependencies are not explicit, a developer error could cause an infinite recursion (e.g. a thunk inadvertently calls itself later), causing pages to hang. This is especially true when you scale your organization and have multiple teams working on the same codebase—one team may inadvertently cause an infinite recursion because they didn’t know how their code affected other teams’ code. As a corollary to Conway’s Law, this specifically happens when there’s coupling: two teams and their code depend on each other.
I’ve actually dealt with this in production multiple times. The worst aspect of this error is that it’s purely client-side, so a simple deployment won’t fix it—you’ll have to tell your users to force exit their browser and clear their cache. It’s also pretty difficult to debug—you’re digging through multiple thunks that interact with each other without any explicit dependencies. It’s like trying to understand the Dark timeline (spoilers) while a gun is pointed at your head.
Figuring out how to split teams without coupling them or their services is a difficult problem to solve as splitting teams involves much more than just technical considerations. However, you shouldn’t make this a more difficult problem by relying on frameworks that make code coupling easier.
Don’t use redux-thunk
There isn’t a good reason to use redux-thunk
. It’s fine for small projects, but if you have to scale technically or organizationally, you’ll run into architectural problems. It’s best to avoid it from the beginning by not depending on it at all.