Hence, after some Google research, we settled on Flux. We liked Flux. It was encouraging that it was specifically addressing the confusion we had been facing and it introduced us to the concept of actions and stores. Actions describe simple mutations to state, and stores encapsulate that state. For instance, an “Add 10% interest” action might update the amount of money held by a store representing a bank account.
Most importantly, Flux was strict about how these actions and stores should interact, using a so-called unidirectional data flow, where React components dispatch actions which update stores which in turn inform the components of any changes to state. When you’re new to a language, strictness is good, as it drastically reduces the number of ways you can shoot yourself in the foot.
With Flux, we were getting pretty good at staying on top of the now-decreased foot-shooting opportunities. But we found we were writing a lot of boilerplate code. Stores had lots of code in common, especially those managing collections of model objects. Furthermore, every asynchronous task had corresponding actions for when that task is starting, when it has succeeded, and when it has failed (and possibly whenever there’s a progress update), all of which were created and handled largely identically.
To stay on top of this, we gradually began refactoring our code, identifying commonalities which could be used by all our stores, and creating increasingly generic abstractions around how we were creating and dispatching our actions, especially our asynchronous ones. It was around this point we coincidentally (and mercifully) discovered Redux, and found that it essentially represented the result of taking our refactoring to its logical extreme.
Like Flux, Redux operates using the concept of actions and stores, but differs in a few crucial ways. Most importantly, in Redux there is only one store, a global store, acting as a single point of truth as to the state of an app. Again, actions represent the mutation of state, but in Redux just a single function — a reducer — is responsible for updating the global state object in response to incoming actions, with each action handled sequentially, and each acting only upon the output state of its predecessor.
On the face of it this sounds unmanageable. Just a single function responsible for handling potentially thousands of actions? In practice most parts of the global state are somewhat independent from all other parts, and the reducer function can be composed from multiple sub-reducers in a process termed reducer composition. Furthermore, in writing a reducer you are quite literally defining the shape of your state object, and it becomes glaringly obvious when two separate parts of state are in fact near-identical and hence can share a reducer, as is the case, say, for asynchronous data, or for lists of data.
Crucially, because all actions pass through a single pipeline and state is held in a single object it is amazingly easy to reason about the effect an action will have. It also makes it trivial to plug-in new functionality and intercept and react to dispatched actions in custom ways. Functionality of this kind is referred to as middleware, and can be quite breathtakingly powerful. In fact, it was middleware which really hit home the true utility of Redux to our team. Because just a vanilla implementation of Redux already aids immensely in code clarity and structure, it felt almost as if we were getting all the auxiliary benefits — benefits such as hassle-free testability, persistence, and crash reporting — for “free”, with very little development effort on our part.