Understanding Flux

A primer on UI architecture with React

At the recent F8 developer conference, Tom Occhino, Pete Hunt and Jing Chen from the Facebook engineering team gave insight into the architecture that powers some of the social network’s web products.

Rethinking Web App Development — Tom Occhino, Pete Hunt and Jing Chen discuss application architecture with Flux & React

The approach they discuss is called Flux: not a framework, but a set of guiding principles for building scalable, maintainable applications with React. It is a prescriptive model for reasoning about web interfaces, but reassuringly simple. Its driving motivation is that interfaces should be predictable; there should be a deterministic process that governs how a particular application state is presented to the user.

Flux is intended for use in tandem with React, though this is not a hard requirement. For the purposes of this post, however, I will assume that React is our view abstraction of choice. React itself is well understood: a declarative view framework that has secured a solid mindshare in the frontend developer community. Battle-hardened at Facebook, it promises to take the anguish out of delivering consistent, predictable interfaces, much in the same way that jQuery has been a panacea for browser compatibility.

“Flux is a system architecture that encourages single-directional data flow through your application” (Tom Occhino)

The core idea behind Flux is that data should flow unilaterally through an application: that is to say, actions and data transformations can go through one or more dispatchers and propagate out to the views, but never in the opposite direction. The view layer is not permitted to modify state directly — it must send a fire-and-forget instruction to a dispatcher, thereby triggering a state change that can then propagate outwards. With this approach, views have no responsibility other than to render the current state of the application: they are not guardians of state, nor should they be.

As promising as these concepts sound, they are too abstract to be entirely useful. What follows should hopefully express these ideas in a more practical light.

Putting words into pictures

Figure 1. shows an architecture diagram for a data-rich, realtime trading dashboard we are building at Football Radar. The purpose of the application is largely incidental, however: this model is appropriate for the vast majority of web applications.

Figure 1

At its heart is the premise that a web application should be a composable set of reactive modules. Data should be able to cascade through an application in a very predictable way. In this system, that means routing everything via two message buses that bookend the dispatcher and storage modules.

The system that Flux proposes — at least in the F8 talk above — is more simplified than this, but the additional abstraction provided by a messaging system allows for a decoupling that will benefit the application as it grows.

This design adheres to the rule of Flux — the single directional flow of data — and is very easy to reason about; the application is completely deterministic, and each component is only loosely coupled to its rest of the system. Where a view wishes to update application state, the instruction must travel via the dispatchers and storage layer, not update state directly.

This is a bit of a high-level interpretation of the architecture, however. Let’s look more specifically at what the various components actually do:

Message Buses (1) & (2)

These are the glue that holds the whole application together. Bookending the dispatcher and storage layers, these two message buses relay instructions to the dispatchers and state changes to the views. They provide a level of indirection that is not strictly necessary, but their presence decouples the application and allows for easier composition.

The API is intentionally naive; only two methods are implemented: send and recv. Any callback bound to the bus will receive all sent messages, and can choose to act upon them or ignore them as appropriate. This is not a topic-based PubSub — there is no routing key — but instead fanout communication with one or more receivers.

bus.send(<ACTION>, <ARGS>);
bus.recv(function(action, args) {
// do something in response to action
});

In spite of its simplicity, it serves a very useful purpose. In our application, we can now add and remove dispatchers where necessary, and no other part of the application needs to know anything about it. We might, for example, decide that the data dispatcher is too generalised and we want to add new dispatchers for each different type of data. With the bus as mediator, nothing needs to change: we add a new dispatcher and it all just works.

The reason for having two message buses instead of one is to ensure a clean separation of responsibility. Moreover, they have a complementary role: if we think of the dispatchers and storage as a huge black box that somehow manages state, then the message buses are an interface into their world. They are the input and output streams to our application.

DataDispatcher & Storage

These create a reactive pipeline for global application data and state. Data that is retrieved or streamed from backend sources feeds into and out of these components.

The DataDispatcher behaves as a routing middleware: messages that are received from the input message bus are delegated to the appropriate storage collection. As per the Flux brief, the dispatcher wraps a promise-driven queue, ensuring that messages are handled predictably.

var Dispatcher = {
dispatch: function(action, message) {
return Promise.all(
this.getCallbacks(action).map(function(callback) {
return callback(message);
})
);
}
};

This is obviously a code stub, but we can easily assume that the finished dispatcher provides a simple accessor for getting the appropriate callbacks for any given message. The callbacks themselves should return a promise.

The storage objects interact with the dispatcher by registering themselves as callbacks. Something like the following could be appropriate:

Dispatcher.register("CREATE", function(message) {
return new Promise(function(resolve, reject) {
if (Store.create(message)) {
resolve(message);
} else {
reject("Could not resolve message: " + message);
}
});
});

Once the promise queue has been resolved, the final step is to notify the view layer via the output message bus. The final code for invoking a state change could look something like this:

Dispatcher.dispatch("CREATE", {
// ...
}).then(function(data) {
output.send("CREATE", data);
});

ActionDispatcher, Transports & AppState

The action dispatcher behaves almost identically to the data dispatcher above: the difference between the two is largely semantic.

There are two specific roles for the action dispatcher:

  1. The first is to propagate local state changes; whereas the data dispatcher responds to global application data, the action dispatcher is responsible for changes local to a specific user session. Transient state — things like hidden or visible modules, preferences and so on — would come under the scope of local session state.
  2. The second purpose is to communicate user interaction to backend services. In our realtime dashboard, we communicate with the outside world via two transport layers: a request transport (Ajax HTTP requests) and a stream transport (WebSockets). Any communication with a backend service will trigger a response that cascades back through the message bus.

Like the storage and application state, callbacks for the transport modules are registered against the dispatcher. The dispatcher has no understanding of the downstream modules and does not discriminate between them.

Dispatcher.register("USER_LOGIN", function(message) {
return new Promise(function(resolve, reject) {
$.post("/api/session", {
data: message,
success: function(res) {
resolve(res);
},
error: function(err) {
reject(err);
}
});
});
});

Views

The views are likely the most familiar part of the system. Any message that cascades through the output message bus causes React to calculate a diff of its virtual DOM and render changes accordingly.

var Root = React.createClass({
componentDidMount: function() {
var self = this;
bus.recv(function(action, args) {
if (action === "CHANGE") {
self.setState({ ... });
}
});
},
render: function() {
// ...
}
});

In this example, we notify changes at the root node and let React perform a diff against the entire render tree. While this helps simplify the application, it is not especially optimised: for demanding cases, it might make more sense to listen for changes at the various leaf nodes and only invalidate smaller subtrees. Indeed, our trading dashboard does exactly that, as it must handle the performance needs of a particularly chatty data stream.

Is Flux the answer?

In a sense, Flux is the missing puzzle piece in the React ecosystem. React is — and always has been — a view framework, and unapologetically so. Despite fitting so comfortably with established frameworks such as Backbone or even with plain old vanilla JS, there has never before been a strong consensus on the best way to architect applications with React.

Although our foray into the Flux approach has been brief, it feels very promising. It will be very interesting to see how well it works at greater scale, but the gains in application predictability lauded by Tom Occhino have been evident from the very start.


Gary Chambers is a Software Engineer at Football Radar, specialising in JavaScript development. Read more about the work done at Football Radar.