Avoiding Event Chains in Single Page Applications

At my company, we recently finished the fifth iteration over a medium sized, but very interactive and graphical web-app.

We started a first prototype with Backbone and as the project got bigger, expanded to Chaplin with d3.js. Some time ago, we started to use React and liked it so much that we ended up with a pure Flux-React implementation.

The reasons to use React were primarily the performance gains and the simplifying "render everything" approach. Interestingly, after strictly implementing the Flux architecture, our application was a lot easier to understand than the Backbone / Chaplin versions.

This seems strange, since you could argue that React is only a "View" solution, and Flux doesn't really add anything new, besides slapping on a new name to a couple of known concepts.

So what gives...?

Well, Backbone an Chaplin are a fine toolset, however, they give you a lot of freedom. This is in my opinion desirable. However, with freedom comes responsibility. And it turns out that its quite easy to be unresponsible, if nobody tells you how things should be done (something that Flux does).

It's the communication, stupid!

We all use frameworks that provide us with a toolbox that help us build SPAs. With those, making TodoMVC like applications are easy and straightforward.

However, if your app gets more complex, some frameworks will leave you on your own quite quickly (others will carry you a little further by beeing "opinionated"). This makes sense, since more tools won't help you to cope better with the complexity.

It's like with building a small house vs building a skyscraper. The tools and building materials are roughly the same, but building the skyscraper requires a lot more thinking on how to combine the building materials.

So let's assume you have a SPA with 20 models and 30 views. What makes things tough? It's not really rendering those models with the views. This is still easy to do.

But when somebody uses his mouse to click somewhere and expects something to change, things might get tough. If you're lucky, it's just a local action, like a dropdown menu that needs to open. If you're unlucky, it's some complex filter action on your data heavy app that causes three ajax requests and changes to seven models.

The ubiquitous tool to solve the "a lot needs to change" problem: Global events. We have an event bus somewhere and every part of the application can publish and listen to these events.

If your app becomes a mess, your events are likely the cause of it. The problem with events is that they are too easy to implement because there are no restrictions.

Lets assume you're implementing a new model A, and if it changes, model B needs to update too. The solution that will come to your mind first is to fire an event from A directly to B (Backbone implements this with the listenTo functionality).

However, always directly passing an event to dependencies will not scale. If your app reaches a certain complexity, you will not have simple X to Y event passing, but complex M to P, Q, R to S, T to U to V, X event chains.

It's important to understand that the problem is twofold: It's not only about the complexity of the event chain (that you might not be able to avoid). It's the fact that every event chain is unique in its blueprint. Every complex interaction has its own specific event chain that ripples through your app. This will make cognitive load extremly high, since you will have to know a lot of specific things to debug your app.

The worst part about the story is that you naturally fall into the event chain trap. The marginal cognitive cost of a new event increases exponentially, but you will not notice this when adding the event. At that time it seems like an easy solution, because you won't think about all the other events. The full cost of your event chains will materialize when you need to fix elusive bugs due to edge cases after having almost finished your app.

How to avoid event chains

A lot of people probably think that event chains in their app are unavoidable. This might be true technically, but cognitively, they can be reduced or even eliminated by structuring the flow of events.

The Flux Architecture shows one effective way to achieve this. While in a React.js context, the pattern is essentially framework agnostic (and I'm sure that people have been doing this before Facebook).

Instead of a simple event bus, you implement something what Flux calls a Dispatcher.

A dispatcher is pretty much an event bus, but you can (optionally) enforce in what sequence the event is "dispatched" to its listeners.

This means that if you receive an event in say model B, you can demand that another model, say model A, should process the event first. This will help you to reduce the length of the event chain, since model A doesn't need to notify B, since B asks A to process first and can therefore assume that A has already changed.

With event bus:

Event -> Model A updates -> Event -> Model B updates (given data from A)

With dispatcher:

		Event -> [Model B asks Model A to update first
        		  Model A updates,
        		  Model B updates (given data from A)]

The second option is better than the first:

  1. you save an event
  2. the dependency is explicitly stated in the right spot: when Model B receives the event. model A doesn't need to care that model B depends on it.
  3. Coupling is minimized, because model B does not generally depend on model A. It only does when this specific event is triggered.

Ordering the sequence in which the event is sent to the listener makes intuitively much more sense, because even if you have a long dependency chain, every model receives the original event. This is how it should be: The user clicks something, it affects two models, they need to change. It's only a technical nuisance that one model needs to wait for the other to update. Conceptually both models should receive the same event.

To give an analogy, imagine a dispatcher as a prism that is hit by a ray of light (the event). It will split up into its facets (affecting different models differently) and change the state of the app. Preferably, the order in which the different facets are processed does not matter. If it does the facets ask others to process first.

This methodology minimizes the amount of events in your system. Furthermore, the events that remain will closely model a plastic, real user action. This means that if you implement changes to a model, you will always directly know which user interaction is the origin of the change. Much better than having a more generic event (say: "change:[attribute]") from another model.

Instead of a specific dependency tree you will always user the same approach for every action that affects the app globally:

  1. Fire an event
  2. Think about which models are affected by the event.
  3. Listen to the event with every model and implement changes
  4. If a model needs data from another one that is affected by the event, force order
  5. after changes, render your app again

In practice, this looks somewhat like this (I adjusted the terminology of Flux to decouple the pattern from it's specific implementation):

Fire Event

for example because you requested data from the server:

AppDispatcher.fireEvent({type: GET_FILTERED_DAYS, payload: data});
Listen to the event with both models

You realize that this affects two models. Your models will register a callback with the dispatcher on instantiation, so it can receive events from the dispatcher:

	//Model A
    this.appDispatch = AppDispatcher.register((event) => {
        var payload = event.payload;
        switch(event.type) {
            case GET_FILTERED_DAYS:
        	// other events that we listen to

	//Model B
    this.appDispatch = AppDispatcher.register((event) => {
        var payload = event.payload;
        switch(event.type) {
           	// other events that we listen to
            case GET_FILTERED_DAYS:
            	// important: waiting for model A to update
        	// other events that we listen to
Implement Changes

The processFilteredDaysPayload method will change the state of each model. The nice thing is that within processFilteredDaysPayload of model B, you can savely get state from the model A instance, since you can be sure that it already processed the event. Both models then use the notification system of your framework to rerender the Views that are affected by the state change.

Check out the practical implementation of a dispatcher in Flux. See my blogpost here that showcases the use of a dispatcher together with async requests.

This pattern is a little more code than say your simple Backbone listenTo solution. So for small applications it might not be worth to implement such an elaborate pattern. In a big application, it will keep you from going insane.

The dispatcher pattern is not the only way to cognitively eliminate event chains. Generally, you need to enforce rules and paths of communication. There are various strategies to do this, but it is crucial that you do it. More often than not, your framework will not provide a solution for this.

Generally, the important thing is that the communication paths should not be long, generic dependency chains. They should be cognitively short, standardized and predictable (This means usually that technically, the event chains actually become longer). Optimal strategies will differ from app to app, depending on its characteristics.

If you have a codebase where an event triggers another event that triggers another one, I recommend looking for a solution that reduces the complexity. I can assure that it helped us a lot.