Reactive Web Apps – Building a Simple Reactive Web Application

With all the buzz about “Reactive” programming, we decided to try our hand at building a simple Reactive web-app for this year’s Devoxx conference. In this blog post, I’ll talk about how we approached the development of the front-end and how we incorporated Reactive principles.

The Demo

The idea was to create a web-app that would work in the browser or on mobile; making use of modern front-end techniques in combination with Diffusion. We decided on a simple but fun idea; display two “options” that users had to vote for – by voting for a particular option, that option’s vote-percentage would increase. Once it hit 100%, it would win, and a new round would begin. However, the values for each option constantly decrease over time; and the more users are connected, the less an individual vote is worth – so fast and concentrated tapping is required to win a round! This app demonstrates several key features;  how to develop a modern web application using Diffusion’s Javascript client, as well as how to use Diffusion’s server-side features to improve performance for mobile apps.  But it also serves as a useful example of applying Reactive principles in a real-world example.

Going reactive

Firstly, it’s worth defining what we mean by Reactive. From the Reactive manifesto, the key tenets for “Reactive applications” are proposed as such:

  • Event Driven
  • Responsive
  • Resilient
  • Scalable

But in the context of a front-end system, what do these mean?

Event Driven

Let’s start with the first point – Event Driven. Instead of writing chains of synchronous calls or forcing direct interactions between separate components, we should pass async messages on an eventing layer. This completely removes any tight coupling you might have between parts of your application, allowing them to focus solely on the data been passed or received. The real value though is that you don’t need to worry about the implementation of the sender or receiver for each message; to your application, there is suddenly no difference between receiving data from a local store, or from a remote service over the network. Event driven architectures can also make more efficient use of limited resources. Because your code won’t be run until an event is received, you don’t need to worry about your application inadvertently blocking other code for no good reason- this is especially important in Javascript applications, where everything is single-threaded.

Responsiveness

In the context of a front-end system, responsiveness is about responding to user’s behaviour. A truly excellent application provides useful feedback in a timely fashion. The value in being sympathetic to user experience can clearly be seen in numerous examples of successful consumer software – Apple being a prime example. The psychology of UI is important to bear in mind here, as it has tangible impacts on customer satisfaction and consequently their willingness to pay for your services. If your web application has a perceived load-time greater than 3 seconds, you could lose up to 40% of your potential customers.

Resilient / Scalable

Resilient and scalable are two sides of the same coin. When we describe an application as resilient, we mean that it can handle load; that it can cope with failure of services; and that performance and end-user experience remains consistent. This is accomplished via scalable architectures – designing your code to explicitly handle the worst cases, and shield your user from broken edge-cases. In practical terms, this means applying Murphy’s Law – anything that can fail, will, and you need to be designing your application to deal with it. This can mean smart reconnection strategies, performing fewer background tasks when battery is low, or making your UI gracefully handle lack of required data.

It’s important to recognize that all of these concerns inform each other; there’s no hierarchy of importance. There is, however, a common focus – your application’s data and events. A well-defined state model for your application’s network connectivity, for instance, allows you to encapsulate an entire set of possible problems and abstract them away from the rest of your application. The use of events means that same state-model can be driven from any part of your application (or even remotely). This way of thinking about our front-end system let’s us be Reactive, and is a perfect fit for a streaming data service like Diffusion!

Frameworks

The main JS frameworks that push a reactive model are React and Om – but ultimately we decided that they were too heavy-weight for our purposes. For this demo, we decided to try a relatively new framework called Vue, which is significantly smaller and lightweight. Vue has some nice features such as:

  • Event-driven: the component hierarchy communicates via events; this makes it incredibly easy to compose well-encapsulated elements
  • Responsive data:  the application’s data model is declared as plain-old Javascript objects; Vue handles the binding to HTML from JS and makes it automagically responsive to changes.
  • Async DOM updates: updating the DOM is one of the most expensive operations a Javascript application can do. Making all changes to the DOM async allows smart batching of updates, making the UI more responsive and reducing the overhead of rapidly-changing data.

Choose your tools carefully! Vue was good for us – but it may not be suitable to your larger applications. Of course, our application also made use of Diffusion’s Javascript API to provide streaming data from the back-end application and to handle messages from clients. Diffusion’s data streaming in combination with Vue’s data-binding capabilities was the perfect match.

Putting It Together

By focusing on events, it was very easy to declare what state our app is in and to communicate that to the user. Here is a snippet from our main application class that listens to particular game status events that are sent from the server, and then handles as appropriate.

var App = Vue.extend({
    ready : function() {
        var timeoutID = null;

        // When we’re not active (for whatever reason) cancel any
        // scheduled messages
        this.$on('inactive', function() {
            clearTimeout(timeoutID);
        });

        // When the round is running, display a message and then
        // signal that we’re active after a short delay
        this.$on('state-running', function(first, second) {
            this.$broadcast('display', first + " vs " + second);

            timeoutID = setTimeout(function() {
                this.$broadcast('active');
            }.bind(this), 1000);
        });

        // When the round has finished, display a message
        this.$on('state-finished', function(winner) {
            this.$broadcast('display', winner + " won!");
            this.$broadcast('inactive');
        });

        // When the game has stopped, display a message too
        this.$on('state-stopped', function() {
            this.$broadcast('display', "Game stopped.");
            this.$broadcast('inactive');
        });
    },

    ...
});

Ready, steady, vote!

The main part of our demo application are the two vote options. The server is in charge of randomly selecting a pair of options each round, which it will then notify all connected clients of. The current vote percentage data for each option is streamed from the server in response to how much people are voting for one or the other; we need to receive this data and then update our UI in response. Because the vote options change each round, we need to dynamically subscribe and unsubscribe from the relevant data sources depending on the game state.  

var Voteable = Vue.extend({
    data : {
        name : "",
        topic : ""
    },
    attached : function() {
        // Create a radial chart for this element
        var chart = new RadialChart(this);
        // Bind an update function to this instance
        var update = function(value) {
            graph.draw(parseInt(value, 10));
        }.bind(this);

        // When the game is active, subscribe to the remote value
       this.$on('active', function() {
           client.subscribe(this.topic, update);
       });

        // When the game is inactive, remove our subscription
       this.$on('inactive', function() {
           client.unsubscribe(this.topic, update);
       });
    },
    methods: {
        // Notify listeners that we've been clicked
        onClick : function() {
            this.$broadcast('voted', this);
        }
    },

    ...
});

When the game is set to an active state, each vote option will subscribe to the topic that has been set when they were constructed; this is bound to a callback that updates a chart. It doesn’t matter what causes the application to be set to active or inactive; each voteable can handle its own internal logic without worrying about any external components. To construct the vote options, and to handle the event that is dispatched when a user clicks the “vote” button, we have a “VoteController” component. This listens to state events to receive the new vote options it needs to construct, as well as listening to vote events. It doesn’t need to explicitly bind anything to the “Voteable” objects – Vue handles all of the event dispatching for us.

var controller = Vue.component('vote-controller', {
    data : {
        votes : []
    },
    ready : function() {
        // The 'state-running' event carries the names for the vote options
        this.$on('state-running', function(first, second) {
            // Vue will handle the binding of this to Voteable instances for us
            this.votes = [
                { name : first, topic : "devoxx/" + first },
                { name : second, topic : "devoxx/" + second }
            ];
        });

        // Listen to vote events and send back to Diffusion
        this.$on('voted', function(vote) {
            client.send("devoxx/vote", vote.name);
        });
    }
});

When the controller receives the ‘state-running’ event, which provides the names of the two options, notice how we only need to add a map of the required data for each Voteable. Vue will correctly bind this to new Voteable instances and inject the data model, which will result in the associated HTML being updated. Notice how all of the code samples deal only with simple data objects and event listeners. Every time we change any data, we can rely on Vue to update any dependencies, constructing or tearing down objects as required. Similarly, we don’t need to worry about how events are implemented under the hood – we only need to listen or dispatch as appropriate and allow our application to respond accordingly. The net result is a codebase that is very loosely coupled and remarkably performant even with high rates of updates to the UI.

Conclusion

I hope this demonstrates that by organising our code around simple but effective concepts, it becomes easy to work with asynchronous data streams. As applications require more and more real-time capabilities, libraries like Vue in combination with platforms like Diffusion are going to become the base for web apps everywhere; allowing us to write complex and powerful applications with ease.