Problems with Flux

I have been writing React/Flux applications for a year now. Looking back at one year of Flux development I can clearly say that this Flux thing works. Past are the days of "I'm not touching that event chain". All the Flux codebases I have worked on were clean, debuggable and maintainable.

With Flux, I was able to solve all problem sets I have encountered, from interactive financial charts, live table filters, regular CRUD stuff to complex async request chains. The generality of Flux is one of its biggest strength. It's never absolutely elegant, but it is never a hack, which is a good thing in large codebases. Applying the same way of thinking when tackling any complex UI is very powerful.

All good?

Of course not. I have noticed time and time again that it is too easy to write Flux code that does not work as intended. The problem is always the same:

You have an action, and unfortunately, it triggers writes in more than one store. And somewhere in one store, you forget to handle the action, or you forget to waitFor on some other store. It almost always slips the test suite. A lot of times, it also slips the code review. It's usually an edge case, so it's not unlikely that the mistake lands in production. Especially waitFor can be very confusing. Other people have raised issues with it as well. But even without waitFor, bugs often happen with complex write.

I would say that having a complex state update triggered by an action is pretty much the last thing that can be hard with Flux and React.

Reducers do not solve the problems

Recently, the "stateless stores" aka "reducers" hit the React community, made popular by Dan Abramov, author of The Evolution of Flux Frameworks and the redux library. There is also an article by Tomas Weiss describing reducers (If you are not familiar with reducers in Flux, catch up now, and return to this article later).

I love the reducers concept. It's elegant and has many advantages over regular store instances. But if I look at the implemented examples, my pain point mentioned above has only partially gone away.

To explain what I mean, let's articulate why waitFor so problematic:

As a developer, you want to declare the order of state updates in one place explicitly line after line. You do not want split the declaration of writing order over multiple modules. There is a lot of mental overhead in saying "this should happen before that" the waitFor way. It's very unfortunate when you cannot state the order of operations by merely writing a statement a line below another to declare execution order. (Ironically, that's precisely the reason Flux splits all updates into atomic, synchronous actions).

For example with Redux, you can do this by composing reducers. But that's not the full solution here. I have declared my state update order more intuitively, but I want more:

As a developer, I wish to have an overview of all the state updates an action triggers. In one place in my code. Line by line.

Having a comprehensive summary somewhere in the code maximizes the possibility of getting the state update correct, especially if the order of the write operations is relevant!

With all Flux implementations I have seen, You have to grep for the action type and maybe browse through multiple files to make sense of a complex write operation. You will be looking at different switch statements that also handle actions you do not care about at all. Having to do that sucks for the developer, and for the person doing the code review, it is even worse. Decentralized action handling has been a tremendous source of mistakes in my projects.

I believe that Flux as of today splits the store/action relationship matrix on the wrong axis: You can easily see all the actions that affect one part of your state. But it's hard to figure out all the state one action affects.

In my opinion, it should be the opposite.

I would argue that naturally, the developer thinks in terms of atomic actions first and foremost. He implements one action after the other, thinking about what state updates the action causes. You do not gain much if I'm able to look up what actions affect a particular slice of the state. But wouldn't you love to see an overview of which updates your action causes to the state? It's the thing that should jump at your eyes when implementing a feature, and even more so in the code review.

I won't have that in every Flux app I've seen so far. Everybody tends to write small stores/reducers because the write operations include business logic. To stay organized, you would like to separate that code into different small modules. Having small state-handling modules means that some actions get handled in many of them, and you're often asking yourself "what does this action cause exhaustively ?" (even if writing order does not matter).

As far as I have seen, everybody writes reducers that handle one small state slice, but all possible actions, like it is a traditional Flux store. For example if you have a todo list in your app, you would somewhere declare todo(state, action) { ... } to reduce all actions that affect the todo state slice.

Here is my question:

Why?

To quote Facebook from their Flux site on the reasoning:

Letting the stores update themselves eliminates many entanglements typically found in MVC applications, where cascading updates between models can lead to unstable state and make accurate testing very difficult.

Well, reducers change state from the outside and work just as well. It's true that Flux does eliminate entanglements typically found in MVC applications, but I don't think it's because they are changing themselves. In my experience, it's because you put every write strictly into the context of a named action. This to me is the big enhancement Flux brings.

The big problem with UI State is this: The dependencies between different state objects change dynamically with the action context. In the context of one action, state object A depends on state object B. In the context of another action, the same state object A might be completely decoupled from B.

For example, this is precisely the trap you fall into when you use Backbone style model change events. Model A listens to updates from model B, without taking the action context into consideration. Static patterns to express dependencies like observers between models do not work well in UI programming because dependencies change dynamically with the context of the required change.

Wanting an overview over all writes of an action is caused by the importance of the action context that determines the relevant state to be changed and the dependencies between state objects. Flux does this reasonably well, as you have to react to a named action in every store. But why not go all the way with this? With a global state tree and reducers, we can go even further.

Reducers do solve the problems!

Why don't we make reducers so that they potentially update any state? With a little composition, You could then declare all the update operations of an action in one place, establishing the dependencies between the different state objects if necessary. This means you handle every action only once.

Let's say you have a global AppState thing that is a prerequisite to using reducers. You just register one reducer that handles all possible actions and all state:

export function appStateReducer(state, action) {  
    switch (action.type) {
        case ADD_TODO:
            return TodoState.addTodo(state, action.text);

        case DELETE_TODO:
            return TodoState.deleteTodo(state, action.id);

        case COMPLEX_ACTION:
            var state = FooState.writeSomeState(state, action.fooId);
            state = BarState.writeStateGivenFooState(state);
            // some complex reducer that resets a a bunch of state to initial state
            return CompositeReducers.resetFiveStateObjectsToInitialState(state)

        default:
            return state;
    }
}

Note the COMPLEX_ACTION where the BarState write depends on the FooState write.

You might think that this does not scale as your app grows, but I think this is not true. The trick is to use other reducers to make the updates expressive and hide implementation and business logic. You would still implement the reducers that are concerned with writing particular state slices in separate modules.

So instead of writing one reducer per "state slice module" that switches over all actions, you just write a reducer for every write operation. For example, in your TodoState module you would have:

export function addTodo(appState, text) {  
    var todos = state.todos;
    state.todos = [{
            id: (todos.length === 0) ? 0 : todos[0].id + 1,
            marked: false,
            text: text
        }, ...todos];
    return state;
}

Implementing the actual operation of adding a todo, but no todoReducer. You would then use your addTodo reducer in the global app state reducer. The nice part about this is that you can choose the interface of your reducers freely, making the dependencies even more explicit with boolean arguments.

There are many good things about this approach:

It's quite similar to what Elm does. Check out this, and the clear overview of the actions and their effects on the state.

It's easy to do this with - say - Redux: You just register one global app reducer that composes other, unregistered reducers. However, you probably won't even need a dependency, as it's only a couple of lines of code to write your global state store that handles your single reducer.

There are innumerable ways you can implement a system with the above properties: You could still implement multiple reducers that do not handle all actions but only certain action groups. You could even go as far as declaring a reducer per action and avoid the switch statement. The important point is to first-order arrange the writes conceptually by action, and not by state slice. This way every action gets handled exactly once, making your compound writes much more comprehensible.

The longer I think about it, the more strange it seems to switch over actions to describe all updates to a particular, small slice of your state in one reducer. I think for everything but reasonably large apps, it does not help much to "shard" your action handling over state slices. In other words: You should have only one or very few store-like entities that switch over all possible actions.

I guess it's a little easier to abuse the system by writing very confusing writes with reducers. That seems a manageable issue for a lot of teams to me.

On the other hand, the action context can be very tricky even for experienced developers. It must be emphasized as much as possible, and a centralized context setting seems much easier to understand than a decentral one. With the composition of reducers, you will be able to handle the dependencies of every context very elegantly.

Maybe I missed some drawbacks here and I will describe them in a year from now. But one thing I'm certain of: You gain a lot of freedom with reducers. We should probably explore different ways of organizing reducers instead of just sticking to the old Flux way.

comments powered by Disqus