Robert Dickert

Personal blog

Why Is My Meteor App Not Updating Reactively?

| Comments

Understanding Meteor’s Deps Package

Have you ever tried to add something to a Meteor template and been frustrated that it didn’t update the way you expected (or at all)? Meteor is often so “magical” that we rely on it without understanding its fundamentals. When we advance beyond simple use cases, we may wonder why the magic stopped, and it can be difficult to know where to begin looking.

Meteor is designed to allow beginners to be productive quickly while making it easy for more advanced users to access more powerful and deeper features. So let’s pay off some of that technical debt by unpacking how Meteor client-side reactivity works in detail. As we do this, we will touch on some common misconceptions.

How Meteor Reactivity Works

What is reactivity?

We will define reactivity here as the ability of Meteor (or any system) to automatically update the values of variables and objects when the information they depend on changes.

The most widely-known example or a reactive system is a spreadsheet. Let’s say you have a spreadsheet with a cell A1 that contains a number and another cell B1 that runs the formula =A1+1. If A1 is set to 1, we expect B1 to change to 2. If we then change A1 to 4, how will cell B1 show the correct value of 5? Clearly, B1 needs to run its formula again. On the other hand, running when, say, C5 changes would be ineffective and wasteful. So, we need (1) a way to run B1’s formula over and over again, and (2) a way to tell B1 when to rerun. Meteor has a very clever way of achieving this with the minimum amount of reruns. This is most visible when Meteor updates templates in real-time, but you can run any code reactively.

Which part of Meteor are we talking about?

Meteor has “full stack reactivity” as one of its Seven Principles, but it’s not a single monolithic system. Meteor is built out of independent packages that work so nicely together that they may be hard to tease apart, but reactivity is accomplished with a number of technologies, including events, callbacks (sometimes abstracted as node fibers on the server), publish/subscribe, and the Deps package for client-side reactivity. The Deps package is the focus of this article.

Meteor client-side reactivity is explicit

What does this mean? Nothing in Meteor is reactive unless it is specifically wired up to make it so. It would be easy to picture reactivity as a single watcher keeping track of all data structures and checking dependencies whenever a change is detected. This is not how Meteor works (although it may be closer to how Angular’s $watch works, by point of comparison).

Instead, Meteor sets up triggers to each data source as it is accessed. When a reactive data source changes, it tells the code that depends on it to rerun. Think mousetraps or dominoes, not a motion detector. This is known as the observer pattern.

Reactive contexts and reactive data sources

When we talk of a reactive context, we are referring to the function that needs to be rerun. Meteor uses an object called a computation to store and run this function, but we will use the terms computation and reactive context somewhat interchangeably. “Running in a reactive context” refers to a function running inside a Meteor computation. If you access a reactive data source in a reactive context, the code will rerun in response to changes to that data source.

A reactive data source is any provider of data that follows Meteor’s contract for providing reactivity. The data source is responsible for making reactivity work. You can add this capability to any object, but if you don’t, reactive updates will not occur, regardless of whether you are in a reactive context.

The data source is in charge

Here’s the clever bit: Meteor puts the data source in charge of watching these data dependencies.

In a reactive context, when you make a call to a reactive data source to read/get data, Meteor sets up callbacks to rerun the computation when you or someone else writes/sets a change to that data. It is the data source that stores and runs these callbacks. No callbacks, no reactivity.

If a reactive data source’s get method is called outside of a reactive context, it returns a result but does not store a callback. Without a computation to register, there is no way for it to know what code to rerun, as the only time we know both sides of the data source-consumer relationship is when we get from the data source. A benefit to this behavior is that you can use reactive data sources normally in non-reactive code with no side-effects.

Meteor’s reactivity contract

Reactivity in Meteor requires cooperation between the code to be rerun and the data sources that will tell it when to do so. The computation and the data source have specific roles that can be described as a contract between them. This is managed by the Deps package, which provides a Deps global object to give you the API you need. Many Meteor components follow this contract out of the box – the source of the “magic” – but it is not difficult to add your own.

To fulfill its side of the contract, the function to be rerun is placed in a computation. Some things such as templates and their helpers are automatically run in a computation, but you can put any function fn in a computation by calling Deps.autorun(fn). The computation stores the function and provides an invalidate() method. When you call the invalidate() method of a computation, it will rerun its function. If the name ‘invalidate’ sounds confusing, consider as an analogy a cache on a web server. You might cache copies of your home page to serve it faster. When your page changes at the source, the cache is said to be invalid because it no longer reflects the master and must be repopulated. So when any of a computation’s data sources change, the computation is considered “invalid,” and it must be rerun. Each computation also has a stop() method that will cause it to cease being reactive. The stop() method is important for cleaning up computations that are no longer needed.

The other side of the contract is fulfilled by the data source. It must keep a list of all the computations to rerun if it changes. These are managed with a Deps.Dependency object, which contains and manages the list of computations. It has a depends() method which adds a computation to its list. It also has a changed() method that will rerun all the functions in its list by calling their invalidate() method.

Make Any Data Source Reactive

Many developers will find they are happy with the reactive data sources already at their disposal, so you may not need to create reactive data sources. That said, learning the source’s side of the reactivity contract is easy and worthwhile. Let’s look at some code.

A reactive data source is responsible for keeping track of which computations called it. These are its dependencies (and the source of the Deps package name). Meteor will manage the list of dependent computations for you, but you must choose where to store it, when to add a dependency, and when to trigger invalidation.

Get started

Say you have a gaming smart package that can put a user into a bonusMode that can be set to ‘none’, ‘double points’, ‘speed’, or other modes. Here is a basic (non-reactive) object to accomplish this with basic get and set operations, along with a couple of autorun statements and a template helper to display results. If you run meteor create depsdemo from the command line, you can just replace the code in depsdemo.js with the following to try it out for yourself.

depsdemo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (Meteor.isClient) {

  bonusMode = {
    mode: 'normal',
    get: function () {
      return this.mode;
    },
    set: function (newValue){
      this.mode = newValue;
      return this.mode;
    }
  };

  Deps.autorun(function (){
    console.log('bonusMode is now:', bonusMode.get());
  });

  handle = Deps.autorun(function (){
    console.log(bonusMode.get(), 'is your mode. (play with my handle)');
  });

  Template.hello.greeting = function () {
    console.log('template helper ran.');
    return "You are in " + bonusMode.get() + " mode.";
  };
}

Note that we are doing this only if (Meteor.isClient), as Deps doesn’t work on the server. When you refresh the page, you will see the log messages in the console and the the greeting rendered in the template. You can run bonusMode.get and bonusMode.set in the console, but the autorun code never reruns, and the template doesn’t update.

Add reactivity

Now let’s make bonusMode reactive:

Adding reactivity to bonusMode: 3 changes
1
2
3
4
5
6
7
8
9
10
11
12
13
  bonusMode = {
    mode: 'none',
    dep: new Deps.Dependency,   //save dependent computations here
    get: function () {
      this.dep.depend();  //saves the Deps.currentComputation
      return this.mode;
    },
    set: function (newValue){
      this.mode = newValue;
      this.dep.changed();  //invalidates all dependent computations
      return this.mode;
    }
  };

We had to add three lines to this code. We created a Deps.Dependency instance and stored it in bonusMode.dep (you don’t have to store dependencies on bonusMode, but it’s a logical choice). The bonusMode.dep object will manage the list of computations for us. When code in a computation calls bonusMode.get(), we need to store its computation. We do this by calling this.dep.depend(). Finally, when we call bonusMode.set() with a new value, we need to tell all the dependent computations that the value has changed and that they need to rerun. We do this by calling this.dep.changed(). That’s really all there is to it. At this point, templates and other computations will update any time bonusMode changes. Go ahead and try it out.

Digging deeper into reactivity

Once you have the above code working, you can tune how it triggers. For example, what happens if you type bonusMode.set('speed') into the console two times in a row? The log messages trigger each time – even though we “updated” to the same value the second time. We are rerunning too often. This is easily fixed by changing the set function:

1
2
3
4
5
6
7
set: function (newValue){
  if (newValue !== this.mode) {
    this.mode = newValue;
    this.dep.changed();  //invalidates all dependent computations
  }
  return this.mode;
}

What about the variable handle we set on the second autorun? Deps.autorun returns a handle to the computation itself. In the console, we can manually tell the computation to rerun:

handle.invalidate()

If you are wondering where the computations are stored, you can see it in the console. Type:

bonusMode.dep 

You will see a Deps.Dependency object which contains a _dependentsById object. Expand that object, and you will find three computations, one for each autorun, and one for the template. If you expand the handle object, you will see it is one of the computations in the the _dependentsById object (you can match handle._id to the key in bonusMode.dep._dependentsById).

We can tell all dependent computations to rerun just as the code does. Typing:

bonusMode.dep.changed()

will trigger all of them despite the fact that nothing has changed – you are essentially saying, “I changed something, so rerun your dependents.” You can see from the log statements that the template reran as well, but Meteor is smart enough to not update the template in the DOM because there are no changes. This is handled by the Spark (Meteor 6.6.x and earlier) or UI (newer versions) package.

We can also disable a computation via its handle:

handle.stop()

When you do this, Meteor will no longer rerun the function, and its dependencies will be cleaned up. Running bonusMode.set('double points') at this point will still run the first computation and update the template, but the stopped computation will no longer trigger. handle.invalidate() will also do nothing. You don’t always have to track your computations’ handles, but you will create memory leaks if you generate autoruns dynamically and don’t stop them. Meteor handles stops automatically in the computations it manages.

Troubleshooting Reactivity

To recap: Your code will run in a computation if either of these is true:

  • You are in a Meteor-defined reactive context such as a template helper
  • You pass your function to Deps.autorun()

Getting data from inside a computation will cause the computation to be registered for reactive execution if the data source follows Meteor’s reactivity contract. Reactive data sources include:

  • Session.get()
  • Meteor.Collection.find()
  • Certain Meteor app status objects (such as Meteor.userId)
  • Reactive data sources from outside packages (such as Iron Router’s Router.current() and others)
  • Any data source you make reactive (see above)

If either of these conditions is not met, you will not get reactive updates.

So this will not update reactively:

1
2
3
4
5
var a = 1;
Deps.autorun(function () {
  console.log(a); //source is not reactive
});
a = 2; //no effect

And neither will this, unless you place it in a template helper or other reactive context:

1
2
3
Session.set('a', 1);;
console.log(Session.get('a')); //not run in a reactive context
Session.set ('a', 2); //no effect

To make this code reactive, we need to meet both criteria:

1
2
3
4
5
Session.set('a', 1);
Deps.autorun(function () {
  console.log(Session.get('a')); //console logs 1
});
 //console logs 2

Quiz: will the following work reactively?

1
2
3
4
5
6
Session.set('a', 1);
var x = Session.get('a')
Deps.autorun(function () {
  console.log(x); //console logs 1
});
Session.set ('a', 2); //console logs ???

Answer: x is set using a reactive data source, and x is accessed in a reactive context, but console.log(x) will only run once. Why is this? Because the computation is registered when Session.get() is called. The line var x = Session.get('a') is called outside a reactive context, so Session does not register a callback (remember, it does nothing if it isn’t in a reactive context). x is then called in the autorun block, but it is not itself a reactive data source, and so the the computation will never rerun. A reactive data source cannot somehow pass its reactive properties to another object from outside a computation.

Am I in a computation?

If you are ever unsure, you can check Deps.active. It will be set to true when running inside a computation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Session.set('a', 1);
var x = Session.get('a')
console.log('Before autorun, Deps.active = ', Deps.active); //false
Deps.autorun(function () {
  console.log(x); //console logs 1
  console.log('Inside autorun, Deps.active = ', Deps.active); //true
});
console.log('After autorun, Deps.active = ', Deps.active); // false
Session.set ('a', 2);

Template.hello.greeting = function () {
  console.log('In helper, Deps.active = ', Deps.active); //true
  return "You are in " + bonusMode.get() + " mode.";
};

Deps.active will always tell you if calls to reactive data sources will have any effect. If you set a breakpoint and are paused in dev tools, you can find Deps under the global variable scope, or you can type Deps.active into the console.

Other Possible Surprises and Pitfalls

Computations run the whole function

Meteor intelligently updates only the parts of your template that have changes. This may mask the fact that the function in the computation behind it is running in full. All of the queries and other code in your template or other reactive environment are being rerun. It is the job of a different Meteor package (UIor Spark) to then evaluate what has changed in that data, if anything, and make the smallest possible change to the DOM. If you have performance problems, you can divide your reactive contexts and/or trigger them more carefully to avoid executing expensive code.

You need to .stop() your computations when you are done with them

Some computations are active for the whole life of the client session and can be left in place without problems. However, if you have code that is dynamically building computations with autorun, you should stop() your computations when you are done with them. This will allow the browser to garbage-collect unused objects.

Deps does not run on the server

Deps is a client-side package and is not available on the server. You must make sure your calls to autorun() are run either in a Meteor.isClient block or in code residing in the client directory.

You also should not expect Deps-style reactivity on the server. Although Meteor unites the client and server in using JavaScript, there is still a context-change in terms of the environment and functionality available. There are other tools on the server to make end-to-end reactivity work, but don’t expect server code to act like it’s in an autorun block. Reactive data sources will not rerun your server code.

For example, you cannot expect a Meteor.publish block to rerun when the result of Collection.find() changes (I made this mistake once, so it is not hypothetical!). The appropriate tool in this example is cursor.observe() or cursor.observeChanges().

Conclusion and Further Resources

If you have made it this far, well done! You now know everything you need to know to control client-side reactivity with confidence. We did not cover every aspect of Deps, but you are now in a position to easily follow the documentation. The Meteor docs are quite clear and explicit about what will run in a reactive context and what the reactive data sources are, and now you know the exact implications of that.

For more about the Deps package, have a look at Chris Mather’s excellent screencasts at EventedMind (he covers Deps here and here). You may even wish to look at the Deps package code itself. It is surprisingly concise and well-commented, and it is very nice bit of coding.

Now that you understand this part of Meteor’s magic, you are ready to make some magic of your own.

Comments