The Magic of Meteor's Reactivity

30 July 2020

About a 4-minute read

Reactive programming is one of the strong points of Meteor. For example, it’s what allows your Meteor app’s client views to rerender when data in Mongo collections changes.

Because Meteor is built with reactive capabilities, a new Meteor app gets the benefits of reactive programming right out of the box. But how does reactive programming actually work? In this article, we’ll dive behind the scenes of reactive programming in Meteor, and learn about some common points of confusion and their solutions along the way.

Reactive Programming in Meteor

Let’s take a moment to review reactive programming for those just getting started in Meteor or who haven’t used it before. Commonly throughout Meteor apps, you’ll see code like the following:

Tracker.autorun(function updateTimezone() {
  const user = Meteor.user();
  currentTemplate.timezone.set(user.timezone);
})

In this example, we fetch the current user with Meteor.user(), and update the timezone of our fictional page’s template with the timezone stored on the user object. Our updateTimezone function is passed as the argument to Tracker.autorun, which will run the function immediately, and then run it again any time the return value of Meteor.user changes.

When I first encountered this reactivity, it seemed like magic. How could the autorun “know” when the return value of our call to Meteor.user would be different on the next call, without constantly calling the function? Furthermore, how could it possibly know which parts of our function were reading reactive data without reading our JavaScript and understanding it?

As usual with programming, it’s not magic, but in this case some pretty cool stuff is happening behind the scenes. Let’s take a closer look to see how it works!

Behind the Scenes: Tracker.autorun

Let’s dive right in and see how Meteor handles things behind the scenes, considering again our example with Tracker. Here’s the code again:

Tracker.autorun(function updateTimezone() {
  const user = Meteor.user();
  currentTemplate.timezone.set(user.timezone);
})

And below, we can see the definition of Tracker.autorun. It’s quite short! (I’ve omitted a few of the argument checks for brevity):

Tracker.autorun = function (f, options) {
  constructingComputation = true;
  var c = new Tracker.Computation(
    f, Tracker.currentComputation, options.onError);

  if (Tracker.active)
    Tracker.onInvalidate(function () {
      c.stop();
    });

  return c;
};

When we call Tracker.autorun, it constructs a Computation object. The Meteor docs define Computation:

A Computation object represents code that is repeatedly rerun in response to reactive data changes.

In this case, the code that will rerun is the function setTimezone that we pass to Tracker.autorun. Tracker passes our function (which it calls f) to the new computation, which stores it internally. The computation immediately calls setTimezone.

The rest of Tracker.autorun handles stopping the new computation if the current computation is invalidated.

To see how the autorun reacts to changes in its inputs, we have to dig a little further, into the definition of Computation.

Tracker.Computation

Our code gets the current user information with a call to Meteor.user. We want our computation to rerun when anything about the user changes, so Meteor.user is a “dependency” of our computation.

The Meteor docs define a Dependency for us:

A Dependency represents an atomic unit of reactive data that a computation might depend on. Reactive data sources such as Session or Minimongo internally create different Dependency objects for different pieces of data, each of which may be depended on by multiple computations. When the data changes, the computations are invalidated. […] Conceptually, the only two things a Dependency can do are gain a dependent and change.

In our example, we’ve created one Computation, and it has one Dependency. When the Dependency changes, our Computation will rerun.

Now we’ve defined the core terms involved in reactive programming. But how does our computation “know” when to rerun? And how does the Dependency signal that it is a reactive data source?

Inside the Dependency

Using our knowledge of the relationship between Computation and Dependency, we can move to an understanding of the full reactive programming picture.

The Dependency class contains the secret that makes it all work. When we first call Tracker.autorun, Tracker constructs a new Computation and passes our code to it. Our code runs right away, which is convenient for us, but something more important is happening: during the first run of our code, we read from the reactive data sources we care about, and in doing so, we trigger a special dependency registration behind the scenes.

Each time we read from a reactive source, the source we’re reading calls Dependency#depend on its own internal dependency object and passes in the current computation, indicating that the current computation depends on this source. The internal dependency maintains a mapping of computation IDs to the corresponding Computation objects. When the reactive data source tells the dependency that the data have changed, the dependency loops through its internal collection of computations and invalidates each of them. After invalidating each computation, the dependency also removes it from the list of dependants.

Pitfall: your function must read from a dependency in order to react to its changes

It’s important to keep in mind that from the computation’s perspective, our code is a black box. It can’t read and understand our intent like a human would!

To figure out what our code depends on, the computation assumes that we will read from every dependency we care about, every time our function runs. The computation will depend only on the dependencies it used the first time it ran. If you don’t read from a certain dependency, that dependency can’t rerun your computation.

Tracker.currentComputation and the power of global variables

Reactive data sources aren’t magical after all:

  1. Your function in an autorun reads from the reactive data source, which maintains an internal dependency object.
  2. The data source (e.g., a Mongo lookup) passes a reference to the current computation to that dependency, and the depedency stores it.
  3. The next time the reactive source has new information, like an updated Mongo document, it signals all of the dependencies’ computations that they should rerun by invalidating them.
  4. Tracker reruns all of the invalidated computations on subsequent turns of the event loop

The exploitable detail of this arrangement is JavaScript’s single-threaded runtime, which permits only one function to run at a time. Because the Tracker can safely assume only one function will run at once (and therefore only one Computation will run at once), it can save a reference to the currentComputation as a global variable! You can see the current computation for yourself in Tracker.currentComputation.

Storing the current computation in a global variable means that any reactive data source can find it trivially in order to pass it to internal Dependency objects. Whenever code in a reactive data source is running, it knows that at most one Computation could depend on the result: the global currentComputation.

It’s popular to hate on global variables these days, but the use of a global currentComputation is simplification at its finest. I hope this brief dip into Meteor’s reactive programming was helpful. See you next time!

Comments