The Problem with Redux... And How to Fix It
Redux has emerged as the preeminent framework for building React-based web applications. It perfectly complements React's declarative views with a straightforward and elegant architecture that brings some of the best ideas of software engineering theory (immutable global state, functional programming, Event Sourcing, CQRS, etc.) into the web development mainstream.
"There are only two hard things in Computer Science," goes the oft-cited quote, "cache invalidation and naming things." Software engineers face another constant challenge though: figuring out where to put stuff. Redux makes you feel like an architecture superhero by making it obvious which code goes where while leaving you with a structurally coherent whole.
As Redux matures and is adopted for large-scale production applications, however, it is becoming clear that the framework is just part of a broader puzzle. Two questions in particular are still the subject of heated debate and experimentation: how to handle side effects and how to create fractal applications with truly reusable components.
In this post we outline an approach that offers a convincing answer to both questions by stealing the best ideas from the Elm Architecture and combining them with Sagas, a mechanism for managing long-lived transactions that is a natural choice for dealing with side effects like API calls and logging.
The Importance of Encapsulation
The need for isolated components is well-illustrated by Sébastien Lorber's "Scalable front-end with Redux or Elm" challenge. Because Redux uses global actions, it's hard to interact with components independently (e.g. for two components of the same type). The tendency is therefore to use React's this.state
(which defeats the purpose of Redux's single centralized state container) or to tightly couple components, so that parent components, for example, end up knowing a lot about the internals of the components they contain.
Why is that bad? For starters, tightly coupled components lead to fragile architectures. Change something in one place and unexpected things happen somewhere else. As applications grow, the problems grows worse as components are increasingly interwoven into an ugly Gordian mess.
If you are reviewing code using pull requests, you'll find that developers too often need to modify code across swathes of the application that should really be local. Subsequent changes are therefore much more likely to affect the same code. The result: developers spend their time rebasing, instead of coding, in order to keep up with changes to the common codebase that occur while they wait for their pull request to land.
What's more, interdependencies make it difficult to publish a component for use by others. Mixing and matching generic components is what modern web development is all about, as witnessed by the npm juggernaut that has steamrolled the JavaScript landscape on both the frontend and backend. You can judge a good application architecture by whether you can pull out an arbitrary component and publish it on npm in a way that makes it useful for others. If your components have to know about the details of the code around them, you can't.
Isolated components are also easier to test. It's pretty annoying when you change a parent component and all the tests for the child components break. Truly independent components don't have this problem.
Elmish Redux
As we mentioned, Redux's global actions make it hard to target specific components with specific actions, especially when your app uses multiple components of the same type. You end up with ad hoc action naming schemes that make assumptions about the shape of your component hierarchy and other specific aspects of your architecture. Change something and... oops! A bunch of stuff breaks in unpredictable ways.
Elm provides a perfect remedy for this since actions know about their context. If I have, for example, two counters on a page (the classic Elm example), an action fired to increment one will be automatically tagged with a prefix that identifies it. Actions fired by parents to manipulate their children are automatically wrapped so that the appropriate components are targeted. Shuffle components around in your view hierarchy, or modify their internals in unanticipated ways, and everything keeps working. We strongly recommend reading the Elm documentation to understand more about what makes it so cool.
Wouldn't it be great if we could do the same in Redux? Well guess what... we can! Redux Elm adds composable actions to Redux and is, as you might have guessed, heavily inspired by Elm. (Disclosure: Redux Elm was written by Tomáš Weiss, an exceedingly handsome, charming and intelligent developer who happens, by pure coincidence, to be the co-author of this article.)
Redux Elm leverages all the brilliant thought that went into Elm and maps it into Redux in a taut, consistent way. A few simple functions let you write truly isolated, reusable components. The result is a more scalable and maintainable software architecture, without sacrificing any of the architectural purity that makes Redux so great.
Fractal Sagas and Side Effects
Redux's central idea is that business logic is contained in pure "reducers" that map one immutable state tree into another. The pureness of these reducers introduces another challenge: how do we handle those icky impurities known as side effects?
Various folks have taken various stabs at this problem. One that is attracting a lot of attention is Redux Saga. The idea of sagas is that you separate "long-lived transactions" completely from your reducers, which are designed after all for short-lived, synchronous operations. Because side effects tend to be asynchronous, using saga transactions to emit them is a natural choice.
Check out this example from the Redux Saga README. Actions like USER_FETCH_REQUESTED
come in. They can be processed by reducers as usual to mutate application state, but they can also be handled in a saga to manage long-running asynchronous operations. In this particular case, USER_FETCH_REQUESTED
and its asynchronous responses, USER_FETCH_SUCCEEDED
or USER_FETCH_FAILED
, are all handled in a single function, so execution flow is crystal clear while keeping the reducers pure. As a bonus, any state local to the transaction can be held in a normal JavaScript variable in the saga's closure instead of polluting the application's global state.
What Redux Elm brings to the party is fractal sagas, powered by Redux Saga but instantiated in the scope of a specific "updater" (Redux Elm's version of a reducer). As a result, actions processed by the saga and any side effects it emits are wrapped and unwrapped automatically according to the position of the saga in the component hierarchy. Move a component somewhere else and you also move its saga, so your transactions and side effects keep working without needing to adapt the code.
Tell Me More!
You can read much, much more about Redux Elm in the documentation. Also check out Tomáš's solution to Sébastien's frontend challenge.