Challenging the Flux architecture

The Flux documentation from Facebook claims that the Flux architecture makes it easier than MVC to handle derived data. In this article I want to challenge the Flux architecture, starting with that idea. Not only do I think Flux doesn’t necessarily make that easier, I also think that MVC is more powerful and allows you to employ better solutions for different situations.

This article is based on the Flux documentation, and a few other resources mentioned in the article. Hopefully, by the time you’re reading this article, Facebook hasn’t changed any of the ideas or terminology.

It’s important to note that there have been different views over time as to what MVC should be exactely, but that should be irrelevant as long as there is a clear separation between models, controllers and views, and they interact accordingly. I’ve described MVC shortly in my previous article, but what I want to stress again is that the view is not allowed to modify or tell the model to change as it’s suggested in this video from Facebook (see the arrows from the views to the models). Another important detail is that the newMessageHandler presented in the video (which looks like representing the controller) mustn’t append messages to the chat tab and message view (which look like view components), instead, in a traditional (but not mandatory) approach, the views would subscribe to models updates. Another approach would be for the view to have a reference to the model and rerender the view based on the state of the model (for instance using React). Another detail which is not related to MVC, but, as we’ll see, is important, is the strategy of always incrementing the unseen threads count and then decrement it if it turns out it wasn’t supposed to be incremented. This can easily lead to bugs where you forget to decrease the counter (for instance if a new use case is introduced where the count needs to be handled). Instead, the count should be increased only if it’s required. So, unfortunately, the example described in the video doesn’t look much like MVC. (As a side note, my view on MVC is fairly casual as long as the architecture is reasonable and consistent and the three pillars are clearly separated.)

I want to start with a working MVC implementation of a similar example, but simpler: a list of messages that also displays the count of unseen messages. Note that the count can be obtained by simply filtering the list of messages, by checking if the items are read, and reading the length of the result. However, that would be cheating, because we have to make sure that we’re setting the count ourselves. This also has constant complexity as opposed to the linear complexity of the filtering, which isn’t necessarily very important.

The MVC code is written using the crizmas-mvc framework as it requires very little effort to understand.

The view:

import React from 'react';
import PropTypes from 'prop-types';
import MessagesList from 'js/components/messages-list';
const Messages = ({controller: {messages, markAsRead}}) => <div>
<div>Unread messages: {messages.unreadMessagesCount}</div>
<div>
<MessagesList
messages={messages.items}
onClick={markAsRead} />
</div>
</div>;
Messages.propTypes = {
controller: PropTypes.object.isRequired
};
export default Messages;

So, the view gets a reference to the controller and it displays the list of messages. When a message is clicked, it calls the markAsRead method of the controller.

The controller:

import Mvc from 'crizmas-mvc';
import messages from 'js/models/messages';
export default Mvc.controller(function MessagesController() {
const ctrl = {
messages
};
  ctrl.markAsRead = (message) => {
messages.markMessageAsRead(message);
};
  return ctrl;
});

The controller holds a reference to the model, allowing the view to get access to it. Also, the controller asks the model to mark the message as read.

The model:

function Message(text) {
return {
text,
isRead: false
};
}
const messages = {
items: [
new Message('message 1'),
new Message('message 2')
],
unreadMessagesCount: null,
  markMessageAsRead(message) {
if (!message.isRead) {
message.isRead = true;
messages.unreadMessagesCount -= 1;
}
}
};
const setUnreadMessagesCount = () => {
messages.unreadMessagesCount = messages.items.filter((message) => !message.isRead).length;
};
setUnreadMessagesCount();
export default messages;

A message has a text and an isRead property. With setUnreadMessagesCount we’re setting the initial count of unread messages. When a message is read, markMessageAsRead is called, where we’re checking if the message is not read and then we’re marking it as read and update the count.

Simple as that. But I’m not happy with this solution. In markMessageAsRead we’re setting the isRead property of the message, and it should be the message’s responsibility to do that. In other words, the message object should have a markAsRead method which we would call from markMessageAsRead. This allows us to modify the method over time and execute some other code whenever the message is read. We, then, could make changes in a single place, and we wouldn’t have to find all the lines in the project where the isRead property is set.

But, in this case, let’s assume that a message is interacted with from another context of the application, and by mistake we’re calling the markAsRead method directly from the controller, which means that the count will not be updated. This wouldn’t happen with Flux, because with Flux we would have a single action for updating the isRead property, and the store where the count is updated would always handle this action. Bad MVC! So, how would we fix this?

Let’s complicate things a little. In order for this issue to occur, the controllers should have access to the markAsRead method. So, let’s make sure this is actually needed, but with a more convincing example.

Let’s say the user is managing a list of ideas for an organization. Each item has a button for liking the idea (mark it as a good idea) and one for activating the idea (the active ideas being implemented by the organization in the following period). Also, the user can create new ideas, presumably in a different view. When an idea is activated, it automatically becomes liked, but an idea can be liked without it being active. The list view must display the count of active ideas. An idea can be liked and/or activated on creation as well, before it’s saved as part of the list. This means that we won’t always mark an idea as active in the same context where the list is managed. (Also, let’s assume that the liked status of an idea can not be deduced based on other information when we’re checking if the idea was liked. For instance, you might think that you could mark the idea as liked only when it’s liked directly and then, when reading the status, use a getter that checkes whether the idea was liked directly or was activated. This wouldn’t work if, for instance, we provided the option to unlike, but keep the idea active. Unliking is not relevant in our example, but it’s important that the idea is marked explicitly as liked when it’s activated, and that the status can not be deduced when reading it.)

To summarize, we have ideas that can be liked and/or activated. When activated, the idea also becomes liked. There are two contexts, one containing a list and another one where the idea is created. In the list view the count of active ideas must be displayed.

With Flux, we could have two stores: one for the idea that is being created and one for the list of ideas. We would then have an action for creating a new idea, one for setting its text, one for marking an idea as liked (either the one that is being created or one from the list), one for marking an idea as active (the same as for liking) and one for saving the new idea to the list. Note that, in order to simplify things, for activating (and similarly for liking) an idea we can use a single action. The list store could then check if the idea that is being activated is included in the list and if that’s the case it can increase the active ideas count. This check can be done either by looking up the idea in the ideas array, or by having an isSaved flag on the idea object (or an id). However, the store that handles new ideas would require such a flag, because it shouldn’t check in the other store if the idea exists there.

Assuming that the objects don’t have ids (for instance, if the entire list is sent to the server with a single action), in general, it would be nice for the idea object not to care if it’s part of a list or not. However, the Flux architecture doesn’t recommend against having a flag like this. But a more annoying aspect is that the logic of marking the idea as liked whenever it’s activated needs to happen in both the stores. At a more fundamental level, the issue is that the same action with the same kind of object needs to be handled similarly in two different stores. Of course, the list store also handles the count, but it would be nicer if the list store would only care about the list and not about updating the state of an idea. This is similar to the initial MVC example, where the messages model (holding the list of messages) updates the state of a message. And while it’s possible to encapsulate the logic in a function and reuse that function in both the stores, the same as it was possible to use the markMessageAsRead method from the MVC example in both the contexts in order to make sure the count is always updated accordingly, it’s also possible to omit the fact that the isLiked flag needs to be updated when the isActive flag is updated, in one of the stores, the same as we could omit the fact that the count needed to be updated in the second context in the MVC implementation (and end up calling the markAsRead method directly).

Having two different actions for activating an idea, one for each of the two contexts, would make this issue even more evident.

The possibility of marking a message as read not leading to updating the count of unread messages in one of the contexts is astonishing, the same as the possibility of triggering the action for marking the idea as active not leading to marking the idea as liked as well, in one of the stores. Astonishment is a sign of brittle software design.

The Flux architecture doesn’t automatically lead to saner code organization. A similar astonishment can result in both the Flux and MVC implementations, in the MVC example by calling the inappropriate method from the controller, and in the Flux example by having to duplicate the logic in two different stores.

One way we can fix this in the Flux implementation is to keep the new idea in the same store as the list of ideas. This way, whenever the action for the active state is triggered, the store first updates the liked flag and then updates the count of active ideas, if needed. The downside of this is that the scope of the store is now extended and the store can become bloated. (Note that if Redux is used, the reducer would become bloated as it wouldn’t make sense to talk about a bloated Redux store.)

The way we can fix this in the MVC implementation is by making it clear that there’s a single place where the idea is marked as active and using the observer pattern to update the count, while keeping the separation of concerns.

The idea model:

import EventEmitter from 'js/utils/event-emitter';
export default function Idea(text) {
const emitter = new EventEmitter();
  const idea = {
text,
isLiked: false,
isActive: false,
    once: emitter.publisher.once,
    markAsLiked() {
idea.isLiked = true;
},
    markAsActive() {
if (!idea.isActive) {
idea.isActive = true;
        idea.markAsLiked();
emitter.emit('mark-active');
}
}
};

return idea;
}

The idea model emits an event when the idea becomes active.

The ideas model:

import Idea from 'js/models/idea';
const ideasList = [];
const ideas = {
activeIdeasCount: 0,
  [Symbol.iterator]: () => ideasList.values(),
  add(idea) {
if (idea.isActive) {
updateActiveIdeasCount();
} else {
idea.once('mark-active', udpateActiveIdeasCount);
}
    ideasList.push(idea);
}
};
const updateActiveIdeasCount = () => {
ideas.activeIdeasCount += 1;
};
ideas.add(new Idea('idea 1'));
ideas.add(new Idea('idea 2'));
export default ideas;

The ideas model subscribes to the ‘mark-active’ event emitted by newly added ideas that are not already active. This means that we need to be careful and only add ideas to the list by calling the add method. Therefore the ideas array is accessible only in this module and the ideas model implements the iteration protocol so that the list can be displayed. We could also provide a get method in order to access a single item.

The ideas controller:

import Mvc from 'crizmas-mvc';
import ideas from 'js/models/ideas';
export default Mvc.controller(function IdeasController() {
const ctrl = {
ideas
};
  ctrl.markAsActive = (idea) => {
idea.markAsActive();
};
  return ctrl;
});

The ideas list view:

import React from 'react';
import PropTypes from 'prop-types';
import IdeasList from 'js/components/ideas-list';
const Ideas = ({controller: {ideas, markAsActive}}) => <div>
<div>Active ideas count: {ideas.activeIdeasCount}</div>
<div>
<IdeasList
ideas={ideas}
onClick={markAsActive} />
</div>
</div>;
Ideas.propTypes = {
controller: PropTypes.object.isRequired
};
export default Ideas;

For simplicity we’re not implementing the whole operation of liking an idea and the context of creating new ideas.

Would a similar approach work with Flux? ‘We found that two-way data bindings led to cascading updates, where changing one object led to another object changing, which could also trigger more updates. As applications grew, these cascading updates made it very difficult to predict what would change as the result of one user interaction. When updates can only change data within a single round, the system as a whole becomes more predictable.’ By reading the Flux documentation it’s difficult to say what exactly would be considered a cascading update. For instance, in the flux-todomvc example, a counter from another module is updated by invoking its method from a store. However, the Flux diagram makes it pretty clear that if the counter was another store, calling its method or subscribing to events emitted by it from another store would not be in accordance with the Flux architecture, as the only way a store is allowed to change is by reacting to actions (which are a different category of events than the events emitted by stores). Therefore, the solution we employed in our MVC implementation, while it’s not based on two-way data binding, would not be allowed in a Flux implementation.

The observer pattern is a way of keeping different parts of the system loosely coupled. While a sensible design is maintained, this shouldn’t lead to issues. But if rules are broken, for instance by modifying the model from the view, you can expect anything to happen. It is interesting to observe that, while the way stores react to actions is similar in nature to the observer pattern, in contrast with Flux’s restrictive nature, a store will be notified about absolutely all the actions in the application. As if you were automatically notified about your neighbour’s toothache. This allows a sloppy programmer to break the single responsibility principle, by reacting to a certain action in a certain store that shouldn’t care about that action.

In case the observer pattern doesn’t suit your needs, MVC allows other ways of orchestrating your data. In fact, you could introduce a dispatching layer and turn your MVC implementation into a sort of Flux implementation (although I’m not arguing that it would be a good idea), but with real controllers. In fact, an obvious violation of separation of concerns is represented by controller-views. ‘Occasionally we may need to add additional controller-views deeper in the hierarchy to keep components simple. This might help us to better encapsulate a section of the hierarchy related to a specific data domain. Be aware, however, that controller-views deeper in the hierarchy can violate the singular flow of data by introducing a new, potentially conflicting entry point for the data flow. In making the decision of whether to add a deep controller-view, balance the gain of simpler components against the complexity of multiple data updates flowing into the hierarchy at different points. These multiple data updates can lead to odd effects, with React’s render method getting invoked repeatedly by updates from different controller-views, potentially increasing the difficulty of debugging.’ Fortunately, the documentation warns against a certain problematic use of controller-views. However, there is an essential issue here: the controller is also a view. If it looks like a duck, it might start quacking like one.

Using a controller-view deeper in the hierarchy can lead to situations where user interaction logic influences unrelated controller logic. Let’s say you have draggable sections in your page. And because you’re looking to simplify the reuse of a section with custom logic, you’re using a controller-view to represent that section. Depending on the way your sections are displayed on the page, it’s possible that during the dragging operation the component will be unmounted and a new component will be mounted. If you’re initiating data fetching in the componentDidMount method, you will fetch the data again without having to. Having controllers independent from the rendering process would prevent this kind of scenarios.

The main idea of Flux is turning the application into a pipeline. If something happens at a certain point of the pipeline based on another point of the pipeline, you need to make sure that those two points are properly ordered. For instance, let’s say you want to prompt the user for confirmation before leaving a route of the application in case the user changed something on that page and didn’t save. Typically a router, such as React-router, would provide an API to set a hook for leaving the route. In that hook you can prevent the user from leaving the route based on the state of the store and prompt the user for confirmation. When the user confirms, you can update the state of the store and then initiate the transition again. When the hook is called again, the new state of the store will allow the user to leave the route. Right? Well, it depends.

Updating the store so that the user is allowed to leave the route happens as a result of an action triggered when the button of the confirmation modal is clicked. Assuming that the state of the store is read the usual way, namely through the component props, if after triggering the action to update the store you initiate the transition, the transition hook will be called immediately, but the pipeline will not have gotten to the point where the props were updated, resulting in using the old props and therefore not allowing the user to leave the route, even though the actual state of the store would allow it.

To fix this, you can read from the store directly. This has the disadvantage that sometimes you read from the props and sometimes directly from the store. Another way to fix this is to initiate the transition in componentDidUpdate. This way the hook will have access to the updated props. The disadvantage is that you have to make a controller decision only after the view has been rendered.

The restricting nature of Flux can be advantageous. Redux takes it a step further, disallowing mutations. This prevents sloppy developers, for instance, from mutating the store/model in the render method of the view. Restrictions such as doing things only one way probably makes it easier to debug ‘update-the-counter-oh-I-didn’t-mean-it’ kind of erroneous reasoning. It seems to me that with Flux/Redux the focus in on debugging and not on software design. Imposing high restrictions and having Facebook backing up this architecture can be politically effective in situations where you’re not convinced that the team will be better off without them, nor do you have the time/possibility of investigating it.