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 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
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.
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
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.
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
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.set in the console, but the
autorun code never reruns, and the template doesn’t update.
Now let’s make
1 2 3 4 5 6 7 8 9 10 11 12 13
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
What about the variable
handle we set on the second
Deps.autorun returns a handle to the computation itself. In the console, we can manually tell the computation to rerun:
If you are wondering where the computations are stored, you can see it in the console. Type:
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
We can tell all dependent computations to rerun just as the code does. Typing:
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:
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.
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
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:
- Certain Meteor app status objects (such as
- Reactive data sources from outside packages (such as Iron Router’s
- 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
And neither will this, unless you place it in a template helper or other reactive context:
1 2 3
To make this code reactive, we need to meet both criteria:
1 2 3 4 5
Quiz: will the following work reactively?
1 2 3 4 5 6
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
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 (
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
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.