The Evolution of Flux Frameworks

There has been no shortage of great Flux implementations, such as Flummox, Alt, or Fluxible. Most of them are focused on making Flux easier to use with the server rendering and reducing the boilerplate. They also often provide convenience utilities like higher-order components and asynchronous action helpers. Still, under the hood, many of them are built on top of the original Flux Dispatcher.

Reducing the boilerplate of Flux is often a tradeoff. Some libraries have chosen to forfeit the great properties of Flux in order to be more succinct.

Say, if the actions aren’t plain objects flowing through a central dispatcher, it is much harder to record and replay actions for debugging. If the action type constants are not explicitly specified, and instead are generated from the method names, they might be more difficult to use together with the static analysis tools like Flow. That there is no single Flux library is a good thing, as the acceptable tradeoffs may vary for every team.

Now, unless you’re light years ahead of us and working on Om Next, Relay or something truly reactive, you might actually enjoy using Flux in your applications. If this is the case, you might want to know if there’s any real evolution happening in the Flux world.

Sure, figuring out how to make the isomorphic Flux easy was a step forward, but after that, the changes I have seen were either too cosmetic, or too drastic to call it Flux. The unidirectional data flow is important, but so is the relative ease of use of Flux, even for people who are not very keen on the functional programming yet. I think that if something requires you to understand cursors or observables, it might be great, but it’s not Flux. (I do like observables. But that’s not my point.)

So is Flux evolving? For the first time in many months, I think that the answer is yes. There are two API changes that I have noticed consistently in many new Flux implementations. These changes complement each other, and while they seem cosmetic, they also open up some exciting possibilities that weren’t available before. And they don’t even introduce any new concepts, such as cursors or observables!

I’m sure that Facebook considered these ideas, but decided to focus on the data flow and the ease of initial understanding. However I think that we’ve lived with this simple Flux long enough that we know its downsides, and it’s time to make it right.

The first change is to have the action creators return the dispatched action.
What looked like this:

export function addTodo(text) {
AppDispatcher.dispatch({
type: ActionTypes.ADD_TODO,
text: text
});
}

can look like this instead:

export function addTodo(text) {
return {
type: ActionTypes.ADD_TODO,
text: text
};
}

Of course, we can no longer call the action creators directly, but if you want to support server rendering, you’re likely to wrap them anyway (or have the library do it for you) so they’re bound to a specific dispatcher instance.

Why is it beneficial? It decouples actions from the dispatcher. The dispatcher becomes an implementation detail. It inverts the control, and we’ll see why inverting the control in Flux is important very soon.

How can async action creators work with this pattern? We could let the user return a function for dispatching multiple actions over time.

export function addTodo(text) {
return dispatch => ({
dispatch({
type: ActionTypes.ADD_TODO,
text: text
});
    API.addTodo(text).then(
() => dispatch({
type: ActionTypes.ADD_TODO_SUCCESS,
text: text
}),
() => dispatch({
type: ActionTypes.ADD_TODO_FAILURE,
text: text
})
);
});
}

It’s easy to provide convenience helpers to make this pattern work with Promise or Observable without the boilerplate above.

This change is not so important by itself, but it works great in tandem with another change: making the Stores stateless. This sounds ridiculous, doesn’t it? Isn’t holding the state the point of Stores?

In my experience, it isn’t! Every Store I ever wrote was managing the state but there’s no reason for the Store to own the state. (Om users, please don’t roll your eyes now. If it’s obvious to you, you may as well go read LtU ;-)

What looked like this:

let _todos = [];
const TodoStore = Object.assign(new EventEmitter(), {
getTodos() {
return _todos;
}
});
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
_todos = _todos.concat([action.text]);
TodoStore.emitChange();
break;
}
});
export default TodoStore;

can look like this instead:

const initialState = { todos: [] };
export default function TodoStore(state = initialState, action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
return { todos: state.todos.concat([action.text]) };
default:
return state;
}

That’s what your Store really is: a function telling the initial state and how the subsequent actions transform it. There is no reason for the Store to actually own that state. It might be held in a single big tree, for example, but it would be an implementation detail of the dispatcher. You don’t have to use the cursors. You can keep the “stores” and “subscriptions” mentality if that’s what you like, because it is easy to understand and feels natural to use.

What do these changes buy us? Turns out, quite a few things:

  • Stores and actions are just pure functions. They are easily testable in isolation.
  • Hot reloading. It’s easy to set up true hot reloading for stateless functions, and I’m working on an experimental project showing how both Flux Stores and Action Creators can be hot reloaded without disrupting the application development flow.
  • Easy transactions. The dispatcher owns the state and has the power to revert the application to any previous state without the programmer implementing something like serialize() or deserialize(). This, combined with hot reloading, enables very powerful developer tools.
  • No need for waitFor() because the dispatcher controls emitting changes and can emit them after all the Store callbacks have run.
  • You can subscribe to the changes at the store level, but the dispatcher may also expose cursor-like functionality for the advanced users who need finer-grained updates.
  • You’re specifying what, not how. Just like in React.

There are already quite a few work-in-progress experimental Flux frameworks exploring this (or similar) way of doing things. Here’s a few off the top of my head:

Keep this pattern in mind. You’ll see interesting things created with it.

Show your support

Clapping shows how much you appreciated Dan Abramov’s story.