Don’t Do, React! Understanding Meteor Reactive Programming

Meteor (currently my favorite framework for building web applications) was designed to support transparent reactive programming. You can’t build Meteor apps without understanding how reactive programming works.

In this blog post, I provide an in-depth explanation of reactive programming. I talk about the technical details of how reactivity is implemented in the Meteor framework. I also explain how you can take advantage of reactivity by using the Session object and reactive variables. Finally, I provide you with advice on getting the best performance when using reactivity.

Reactive Programming versus Event-Driven Programming

What is reactive programming? The best way to understand reactive programming is to compare it with a programming style that we all know and love: event-driven programming. You use event-driven programming every time that you write a JavaScript app.

Imagine, for example, that you are building a dashboard for your spaceship. You want to update the dashboard whenever the temperature of your spaceship engine changes so you can prevent the spaceship from overheating and blowing up.

Using a traditional event-driven programming style, you most likely would create the spaceship object something like this:


/************
    Create spaceship
 *************/

var spaceship = {

    _temperature: 0,
    _listeners: [],

    getTemperature: function() {
        return this._temperature;
    },

    setTemperature: function(value) {
        if (value != this._temperature) {
            this._temperature = value;
            this.changed();
        }
    },

    changed: function() {
        for (var i=0;i< this._listeners.length;i++) {
            this._listeners[i]();
        }
    },

    onChanged: function(func) {
        this._listeners.push(func);
    }
};

The spaceship object in the code above contains a getTemperature() and setTemperature() method for getting and setting the spaceship engine temperature. The object also incudes a changed() and onChanged() method for setting up an event handler.

When the setTemperature() method is called, the changed() method is called, and any event handlers registered with the spaceship object are executed. You register an event handler by using the onChanged() method like this:

/************
 Update dashboard when temperature changes
 *************/
spaceship.onChanged(function() {
    // get temperature
    var temp = spaceship.getTemperature();

    // Update dashboard gauge
    gauge.setValue(temp);
});

This event handler updates a dashboard gauge whenever the temperature changes. For example, if you call spaceship.setTemperature(140) then the spaceship.changed() method will be called and the value displayed by the gauge will be updated.

spaceship

You register an event handler so you can be notified when changes occur. This is a very common pattern for detecting and responding to changes in applications.

The reactive approach to solving the same problem requires less code. Here’s the reactive version of the spaceship object:

/************
 Create spaceship
 *************/

var spaceship = {

    _temperature: 0,
    _temperatureDepend: new Tracker.Dependency,

    getTemperature: function() {
        this._temperatureDepend.depend();
        return this._temperature;
    },

    setTemperature: function(value) {
        if (value != this._temperature) {
            this._temperature = value;
            this._temperatureDepend.changed();
        }
    }
};

Notice that our new version of the spaceship object no longer includes changed() and onChanged() methods. Those methods have gone away because you don’t need to explicitly register an event handler when using reactivity.

Instead, the code above uses a Tracker Dependency object. The Dependency object is used by both the getTemperature() and setTemperature() methods.

There is one more chunk of code that we need to update the dashboard when the temperature changes:

/************
 Update dashboard when temperature changes
 *************/
Tracker.autorun(function() {
    // get temperature
    var temp = spaceship.getTemperature();

    // Update dashboard gauge
    gauge.setValue(temp);
});

The Tracker.autorun() method accepts a function. The function is rerun whenever any dependencies referenced by the function changes. The code above calls the spaceship.getTemperature() method which includes a dependency.

When you call the spaceship.setTemperature() method, the Depedency changed() method is called. Calling Depedency changed() causes the function passed to Tracker.autorun() to be rerun automatically.

The beautiful thing about using reactive programming instead of event-driven programming is that it requires less code to cause code to run when there is a change in your application. Instead of wiring up an explicit event handler, the function passed to Tracker.autorun() is rerun automatically whenever any of its dependencies change.

When you use a reactive programming style, reading data is the same as subscribing to change notifications for the data. You don’t need to explicitly declare that you want to rerun a function when something changes – the function reruns automatically.

Using the Tracker JavaScript Library

Reactivity in Meteor is implemented in the Tracker JavaScript library. The Tracker library is a very small library (you can read through it in a couple minutes). You can view the library here:

https://raw.githubusercontent.com/meteor/meteor/devel/packages/tracker/tracker.js

The Tracker library defines two objects:

  • Computation – The Computation object represents a function.

  • Dependency – The Dependency object represents a list of Computations.

And the Tracker library has one important method:

  • Tracker.autorun() – Creates a reactive context by creating a Computation object that is associated with the function passed to the Tracker.autorun() method.

Here’s how the Tracker library works. Calling the following method create a new Computation object and assigns the Computation object to the global Tracker.currentComputation property:

Tracker.autorun(function() {
    // get temperature
    var temp = spaceship.getTemperature();

    // Update dashboard gauge
    gauge.setValue(temp);
});

In this case, the function passed to the Tracker.autorun() method calls spaceship.getTemperature() method. The getTemperature() method create a dependency:

_temperatureDepend: new Tracker.Dependency,

getTemperature: function() {
    this._temperatureDepend.depend();
    return this._temperature;
},

In the code above, the depend() method adds the current Computation object (read from the Tracker.currentComputation property) to the list of Computations tracked by the _temperatureDepend Dependency object.

When you call setTemperature(), the Dependency changed() method is called:

setTemperature: function(value) {
    if (value != this._temperature) {
        this._temperature = value;
        this._temperatureDepend.changed();
    }
}

The changed() method simply iterates through all of the Computation objects contained in the Dependency and calls the Invalidate() method on each Computation object. When the Invalidate() method is called on a Computation, the function associated with the Computation is re-run.

The upshot is that any function that you pass to Tracker.autorun() gets rerun automatically whenever any of its dependencies changes. The dependencies can represent anything: results from a database query, the current temperature of your spaceship, or the name of the current authenticated user.

The Flush Cycle

When a Computation is invalidated, the function associated with the Computation is not rerun immediately. Instead, the function is run as soon as the computer is idle.

There is nothing magic here, the Tracker library simply calls window.setTimeout() with a zero length interval like this:

setTimeout(Tracker.flush, 0);

As soon as the JavaScript thread is not busy, the Tracker.flush() method is called. This method loops through all of the Computation objects that have been invalidated and reruns their functions.

The fact that a function is not run immediately when its Computation object is invalidated has two important implications.

First, you should not expect changes you make to reactive data sources to happen immediately. For example, imagine that you are using a Dependency object to show and hide a modal dialog:

var _showModal = false;
var _showModalDependency = new Tracker.Dependency();

Tracker.autorun(function() {
    _showModalDependency.depend();
    if (_showModal) {
        $('#modalDialog').fadeIn();
    } else {
        $('#modalDialog').fadeOut();
    }

});

Template.modal.events({
   'click #show': function() {
        _showModal = true;
       _showModalDependency.changed();
   },

    'click #hide': function() {
        _showModal = false;
        _showModalDependency.changed();
    }
});

The code above contains event handlers for clicking the show and hide buttons. When you click the show button, the Dependency is changed and the function passed to autorun for showing or hiding the modal dialog is rerun.

You should not expect the modal popup to appear immediately after calling _showModalDependency.changed(). For example, if you attempt to change the title of the modal dialog in the next line of code then you’ll get an exception because the modal dialog won’t be in the DOM.

If you really want the modal popup to appear immediately then you need to call Tracker.flush() like this:

Template.modal.events({
   'click #show': function() {
        _showModal = true;
       _showModalDependency.changed();
      Tracker.flush();
      // Now, you can change the modal dialog
   },

    'click #hide': function() {
        _showModal = false;
        _showModalDependency.changed();
      Tracker.flush();
      // Now, you can change the modal dialog
    }
});

Calling Tracker.flush() forces any function associated with an invalidated Computation object to immediately run. In this case, it would cause the function passed to Tracker.autorun() to immediately run.

The other important implication of the way dependencies work concerns multiple invalidations. Consider the following code:

function changeTemperature() {
   spaceship.setTemperature(1);
   spaceship.setTemperature(2);
   spaceship.setTemperature(3);
   spaceship.setTemperature(4);
   spaceship.setTemperature(5);
}

You might expect the changeTemperate() function to change the temperature 5 times and update the dashboard 5 times. Actually, that doesn’t happen. Changing the temperature multiple times is the same as changing the temperature one time and only the last setTemperature() matters.

If you want each call to setTemperature() matter then you need to call flush:

function changeTemperature() {
   spaceship.setTemperature(1);
   Tracker.flush();
   spaceship.setTemperature(2);
   Tracker.flush();
   spaceship.setTemperature(3);
   Tracker.flush();
   spaceship.setTemperature(4);
   Tracker.flush();
   spaceship.setTemperature(5);
}

Meteor Reactivity is Transparent

Above, I might have misled you into thinking that Meteor’s reactive programming style is more complicated than it is. Typically, you never work with the Tracker Computation or Dependency objects directly. Instead, you work with reactive data sources. Meteor has several types of reactive data sources such as:

  • Session object

  • Reactive variables

  • Minimongo

  • Meteor.user(), Meteor.status()

These objects create Tracker dependency objects for you internally. If you write JavaScript code that interacts with these objects then you get reactivity for free (transparently).

You also rarely need to call Tracker.autorun() yourself. Instead, if you are using the Blaze templating engine – the templating engine used by Meteor – then Tracker.autorun() is called for you automatically. Any template helper that you create is run with Tracker.autorun() by Blaze.

Meteor reactivity is transparent because you don’t need to think about it. If your JavaScript code references any reactive data sources then you get the reactivity for free.
Using the Session Object

The Meteor Session object is a reactive dictionary (that survives hot code pushes). In other words, the Session object contains name/value pairs that create Tracker Dependency objects automatically.

Consider the following template helper function:

Template.session.helpers({

    message: function() {
        return Session.get("message");
    }

});

This helper function returns a message that is displayed in the browser. The message is retrieved from the Session object.

The Session object sets up a dependency so that whenever the value of the message key changes in the Session dictionary, the template helper is rerun automatically. This means that you can change the message displayed in the browser simply by calling:

Session.set("message",  “Hello World!”);

You can change the Session object anywhere in your app and the helper method will run again. You can even open up Developer Tools in your browser and change the Session object from the Console window (I do this all of the time to debug my Meteor apps).

The Session object has four methods:

  • get()

  • set()

  • setDefault()

  • equals()

The set() and setDefault() methods are almost identical. The only difference is that setDefault() will set a value only when the value is currently undefined.

The equals() method is more interesting. By taking advantage of equals(), you can improve the performance of your Meteor app as I describe next.

Session.get versus Session.equals()

To get the best performance in your Meteor app, always use Session.equals instead of Session.get().

Imagine that I want to display a list of movies in a table and I want to be able to edit the movies. In that case, I might create a template that looks like this:

<template name="movies">
<table>
    <tbody>
    {{#each movies}}
      <tr>
          <td>
          {{#if selected}}
              <input id="title" value="{{title}}" />
          {{else}}
              <a id="edit1" href="#">edit</a>
              {{title}}
          {{/if}}
          </td>
      </tr>
    {{/each}}
    </tbody>
</table>
</template>

You can select a movie by clicking the Edit link. If a movie is selected then you can edit the movie title. Otherwise, if the movie is not selected, then the movie title is displayed.

reactSelected

Here’s what the JavaScript code for the movies template looks like:

Template.sessionEquals.helpers({

    movies: function() {
       return db.movies.find();
    },

    // Bad Code
    selected: function() {
        var currentMovieId = this._id;
        return Session.get('selected_movieId') === currentMovieId;
    }

});


Template.sessionEquals.events({
    'click #edit1': function(e) {
        e.preventDefault();

        var currentMovieId = this._id;
        Session.set('selected_movieId', currentMovieId);
    },

});

If you click the edit link next to a movie then the id of the selected movie is assigned to the Session ‘selected_movieId’ key. Because the Session object is a reactive data source, changing the value of the Session object causes the selected() method to rerun.

Now, here is the bad part. If you change the selected movie then the selected() helper is run again for each movie being displayed. If you are displaying 500 movies in your template then the selected() helper is rerun 500 times.

There is a better way of doing this. Instead, of using Session.get() in the selected() method, you should use Session.equals() like this:

// Good Code
selected: function() {
    var currentMovieId = this._id;
    return Session.equals('selected_movieId', currentMovieId);
}

Making this change enables you to avoid rerunning the selected() function for all 500 movies whenever you select a new movie. Instead, the selected() function only needs to be run once or twice: only for the previously selected and newly selected movies.

So, always use Session.equals() instead of Session.get() when you can get away with it.

Using Reactive Variables

The Session object is global to your application. If you want to scope reactivity to a particular template then you can use a reactive variable instead. For example, if you want to display a “page loading” indicator then it makes more sense to use a reactive variable than the Session object.

Before you can use a reactive variable, you must first install an additional Meteor package like this:

$ meteor add reactive-var

Installing this package adds a new object named ReactiveVar This object has two methods:

  • get()

  • set()

Notice that a reactive variable, unlike the Session object, does not have an equals() method. Yes, that is a shame.

Here’s how you can use a reactive variable in a template to indicate when the template is loading:

Template.myTemplate.created = function() {
    // loading
    this.loading = new ReactiveVar();
    this.loading.set(false);

    // movie title
    this.movieTitle = new ReactiveVar();
    this.movieTitle.set('????');
}

Template. myTemplate.helpers({
    loading: function() {
        return Template.instance().loading.get();
    },

    movieTitle: function() {
        return Template.instance().movieTitle.get();
    }
});

Template. myTemplate.events({
    'click': function(e, template) {
        e.preventDefault();

        // show loading
        template.loading.set(true);

        // Call back to server to get movie
        Meteor.call('getRandomMovieTitle', function(err, title) {
            // hide loading
            template.loading.set(false);
            if (title) {
                // show title
                template.movieTitle.set(title);
            }
        });
    }

})

The code above shows a single movie randomly selected from the server. When you click the button, a new movie is displayed.

reactShowRandomMovie

The new movie is retrieved through an Ajax call (by calling Meteor.call() in the code above). Because an Ajax call might take some time, it is a good idea to display a loading indicator while the movie is being retrieved:

In the code above, two reactive variables are created in the template created() event handler. First, a reactive variable named loading is created which represents whether a movie is being loaded. Second, a reactive variable that represents the title of the currently loaded movie is created.

When you click the button to display a new movie, the loading variable is set to the value true and the Ajax call is performed. When the Ajax call completes, the loading variable is set to the value false.

Using Minimongo

Minimongo also uses reactivity. When you perform a database query using find() then the query acts as a reactive data source.

var movies = db.movies.find();

If the movies collection changes then any template helpers that reference the results of find() will rerun automatically. This is super convenient because it means that you can just create a template helper and know that it will rerun automatically whenever the data changes:

Template.minimongo.helpers({

    movies: function() {
        return db.movies.find();
    }

});

By the way, there is an important difference between using db.movies.find() and db.movies.find().fetch() here when using the Blaze templating engine. If you use fetch() then all of the movies are returned as an array. In that case, Blaze must re-render all of the movies whenever there is a change in the movie data. Instead, you should always return data from a template helper using find() instead of fetch(). In other words, don’t do this:

Template.minimongo.helpers({

    movies: function() {
        // bad!
        return db.movies.find().fetch();
    }

});

Minimongo is smart about how it sets up dependencies. If you create a template helper that returns the count of movies then the template helper is not rerun whenever the data changes. Instead, it is rerun only when the count of movies changes:

Template.minimongo.helpers({

    moviesCount: function() {
        return db.movies.find().count();
    }

});

So, if you edit an existing movie then the moviesCount() helper won’t be rerun. However, if you insert or delete a movie and, therefore, change the movie count then moviesCount() is rerun automatically.

Summary

The goal of this blog entry was to explain the reactive programming style embraced by Meteor. I explained the technical details of how reactivity is implemented by explaining how the Tracker Javascript library works. I also discussed several reactive data sources including the Session object, reactive variables, and Minimongo.

Meteor is a realtime web application framework and reactivity and realtime are a natural combination. When building a realtime web application, you want the application to update automatically whenever there are changes. Reactivity enables a Meteor app to update automatically whenever there are changes without requiring you to write a lot of code.

All of the code discussed in this blog entry can be downloaded from Github:

https://gitHub.com/StephenWalther/Nodevember2014

Discussion

Comments are closed.