If you follow React developers on Twitter, you've probably been hearing the name Redux come up a lot lately. There's a good reason for this - Redux solves one of the biggest and most frustrating pain points for most React developers - state management. In this article I'll describe the problem of state management and how I've dealt with it over time, why we chose Redux to make things better, and how Redux works.
Activate the Flux Capacitor!
It's easy for developers to get themselves into trouble with the built-in state management that React provides. It's component-specific, it's mutable and could change at any time, developers often duplicate pieces of state across multiple components, and it ends up being very difficult to follow exactly what's happening when and what side effects it will have.
To ease the pain, Facebook posted more about their application structure, which they call Flux. This architecture is based on a one-way data flow where actions are dispatched to update stores, which triggers a refresh of the views (which are React components). This structure is likely one of the big reasons that React's popularity has grown as quickly as it has. It released developers from the challenge of managing two-way bindings and resolving multiple sources of truth.
The pain wasn't completely alleviated, however. When developing complex interfaces, developers often had to pass dozens and dozens of properties from the top of the tree all the way down to the deeply nested elements that actually cared about that state. Every parent element had to be deeply concerned about what its children needed.
At Craftsy, we started looking for ways to overcome this challenge and make state even easier to work with. At first, we embraced a dispatcher based on EventEmitter2 and used it with Backbone-inspired models that our React components watched. This worked well with very simple components, but as our interfaces become more complicated we soon ran into race conditions and confusing data flows. Our code became a tangled mess that was difficult to debug and maintain.
After seeing libraries like Om and Este.js emerge, I was inspired to address the problem of state management using a global state container. With this approach, you have one central place where state is maintained. You can instrument the state container to gain a better understanding of how state is changing over time. You can save your user's state and restore it later. You can replay state changes on your local machine to debug issues in production.
To avoid the problem of passing numerous props down endless branches of the component tree, I implemented a solution inspired by an excellent post that Dan Abramov had just published at the time - "Mixins are Dead. Long Live Composition". I embraced the idea of higher-order components and created what we called "Bad Parent".
With Bad Parent, parent components didn't have to care at all about what their child components needed. Child components could simply wrap themselves in my
<ListenToState> wrapper component, which would inject portions of the global state into the wrapped component via props. When the state changed, the ListenToState wrapper would trigger a re-render, which would in turn re-render the child components. We began to form the idea of smart components that subscribed to the state they needed, and generic dumb components that simply accepted props to customize behavior and presentation.
Our team loved the Bad Parent structure and quickly began to improve on it as they used it in their projects. They expanded it with the concept of derived properties, and began working on ways to improve the wrapper component using a functional
compose() style that took functions which mapped state to props and defined actions that transformed the state. One change we made that was a step in the wrong direction, in my opinion, was to move from a global state container to multiple state containers. This helped us maintain our rapidly expanding pool of state in a more organized way, but we lost all of the advantages of global state.
The Discovery of Redux
After a few iterations on this, we stumbled across Dan Abramov's Redux. The React bindings with the
connect() function looked surprisingly similar to what we were working on, but the overall structure of state management with reducers and the reselect library for derived data was well ahead of where we were. It combined the organizational benefit of having multiple state managers (implemented as reducers) with the power of a global state container. We quickly embraced Redux and found that it solved all of the challenges we had with our Bad Parent structure, and a whole lot more!
With Redux, state transformations are handled via pure reducer functions. The state is kept immutable and reducers return transformed copies of the state. Side effects are strictly avoided so that your state is always predictable and replayable. In addition, the library is easy to use in the browser, on the server, and even in React Native applications. Abramov couples this with a dev server for React that implements hot reloading - live updates to your components as you edit them with no refresh needed - and time travel - moving forward and backward through state transitions.
The Incredible Machine
Redux works by defining a store - a global state container. When you dispatch an action, the store passes the action to its reducer to generate a new copy of the state that has been transformed based on the action. The reducer can actually delegate to any number of smaller reducers to handle transformations for different portions of the state. For example, you may have a user reducer that handles transformations for the 'user' key in the global state.
Using the react-redux bindings, you can use
connect() to inject portions of the state into a components props, just as we were doing with Bad Parent. The connect function accepts a variety of options, including functions that map state to props, map action dispatchers to props, and more. When the portion of the state your component is connected to changes, your component re-renders with the new state.
To extend Redux's functionality, you can apply middleware that takes control after an action is dispatched and before it is passed to a reducer. One simple yet powerful middleware that Abramov provides is called 'redux-thunk', and it allows for asynchronous action creators. This is the best place to make api calls to retrieve additional data needed for state transformations.
Go Forth and Reduce!
To learn more about how Redux works and how best to use it, set aside some time and read through his very detailed documentation. I'm planning to create a screencast series soon to walk developers through the process of creating a simple isomorphic application using Redux and React, so keep an eye on my blog in the weeks ahead.
If you'd like to support Abramov as he works tirelessly to improve Redux, check out his Patreon page and make a monthly pledge!
If you've got questions for me, you can find me on Twitter as @bkonkle, on Github as bkonkle, or on Facebook as brandon.konkle. I also frequent great Slack communities like Reactiflux, Denver Devs, and TechFriends.
Thanks for reading!