Explicit over implicit
With architectural patterns, the tendency is to make things easier by veiling them behind abstractions that grow more elaborate with time. Eventually, more and more of the system's data changes automatically and developer convenience is superseded by hidden complexity.
This is a real scalability issue, and Flux handles it by favoring explicit actions and data transformations over implicit abstractions. In this section, we'll explore the benefits of explicitness along with the trade-offs to be made.
Updates via hidden side-effects
We've seen already, in this chapter, how difficult it can be to deal with hidden state changes that hide behind abstractions. They help us avoid writing code, but they also hurt by making it difficult to comprehend an entire work-flow when we come back and look at the code later. With Flux, state is kept in a store, and the store is responsible for changing its own state. What's nice about this is that when we want to inquire about how a given store changes state, all the state transformation code is there, in one place. Let's look at an example store:
// A Flux store with state. class Store { constructor() { // The initial state of the store. this.state = { clickable: false }; // All of the state transformations happen // here. The "action.type" property is how it // determines what changes will take place. dispatcher.register((e) => { // Depending on the type of action, we // use "Object.assign()" to assign different // values to "this.state". switch (e.type) { case 'show': Object.assign(this.state, e.payload, { clickable: true }); break; case 'hide': Object.assign(this.state, e.payload, { clickable: false }); break; default: break; } }); } } // Creates a new store instance. var store = new Store(); // Dispatches a "show" action. dispatcher.dispatch({ type: 'show', payload: { display: 'block' } }); console.log('Showing', store.state); // → Showing {clickable: true, display: "block"} // Dispatches a "hide" action. dispatcher.dispatch({ type: 'hide', payload: { display: 'none' } }); console.log('Hiding', store.state); // → Hiding {clickable: false, display: "none"}
Here, we have a store with a simple state
object. In the constructor, the store registers a callback function with the dispatcher
. All state transformations take place, explicitly, in one function. This is where data turns into information for our user interface. We don't have to hunt down the little bits and pieces of data as they change state across multiple components; this doesn't happen in Flux.
So the question now becomes, how do views make use of this monolithic state data? In other types of frontend architecture, the views get notified whenever any piece of state changes. In the preceding example, a view gets notified when the clickable
property changes, and again when the display
property changes. The view has logic to render these two changes independently of one another. However, views in Flux don't get fine-grained updates like these. Instead, they're notified when the store state changes and the state data is what's given to them.
The implication here is that we should lean toward view technology that's good at re-rendering whole components. This is what makes React a good fit for Flux architectures. Nonetheless, we're free to use any view technology we please, as we'll see later on in the book.
Data changes state in one place
As we saw in the preceding section, the store transformation code is encapsulated within the store. This is intentional. The transformation code that mutates a store's state is supposed to live nearby. Close proximity drastically reduces the complexity of figuring out where state changes happen as systems grow more complex. This makes state changes explicit, instead of abstract and implicit.
One potential trade-off with having a store manage all of the state transformation code is that there could be a lot of it. The code we looked at used a single switch
statement to handle all of the state transform logic. This would obviously cause a bit of a headache later on when there's a lot of cases to handle. We'll think about this more later in the book, when the time comes to consider large, complex stores. Just know that we can re-factor our stores to elegantly handle a large number of cases, while keeping the coupling of business logic and state tight.
This leads us right back to the separation of concerns principle. With Flux stores, the data and the logic that operates on it isn't separated at all. Is this actually a bad thing though? An action is dispatched, a store is notified about it, and it changes its state (or does nothing, ignoring the action). The logic that changes the state is located in the same component because there's nothing to gain by moving it somewhere else.
Too many actions?
Actions make everything that happens in a Flux architecture explicit. By everything, I mean everything—if it happens, it was the result of an action being dispatched. This is good because it's easy to figure out where actions are dispatched from. Even as the system grows, action dispatches are easy to find in our code, because they can only come from a handful of places. For example, we won't find actions being dispatched within stores.
Any feature we create has the potential to create dozens of actions, if not more. We tend to think that more means bad, from an architectural perspective. If there's more of something, it's going to be more difficult to scale and to program with. There's some truth to this, but if we're going to have a lot of something, which is unavoidable in any large system, it's good that it's actions. Actions are relatively lightweight in that they describe something that happens in our application. In other words, actions aren't heavyweight items that we need to fret over having a lot of.
Does having a lot of actions mean that we need to cram them all into one huge monolithic actions module? Thankfully, we don't have to do this. Just because actions are the entry point into any Flux system, doesn't mean that we can't modularize them to our liking. This is true of all the Flux components we develop, and we'll keep an eye open for ways that we can keep our code modular as we progress through the book.