meteor: scoped reactivity
Raise your hand if you've experienced this problem - you have a template that works as expected, but it breaks as soon you render two of them simultaneously.
The most likely cause is that your template used a session variable to store its state. If all instances of a template share the same state, they cannot act independently. To illustrate this, let's look at a simple example.
Color square
<body>
{{> colorSquare color1='green' color2='purple'}}
</body>
<template name="colorSquare">
<div class='color-square' style='background: {{currentColor}}'></div>
</template>
We'll render a square which, when clicked, will alternate between two colors. If this syntax is unfamiliar to you, I'd recommend reading spacebars secrets.
Let's step through the template code. First we'll store the initial color into a session variable called currentColor
. Note that the onCreated
and onRendered
callbacks have access to the template's data context through this.data
. Here currentColor
will be 'green'
.
Template.colorSquare.onCreated(function() {
Session.set('currentColor', this.data.color1);
});
Next, we need to alternate colors when the user clicks on the template.An event gets access to its template's context through its second argument. In this case, template.data.color2
is 'purple'
.
Template.colorSquare.events({
click: function(e, template) {
var color1 = template.data.color1;
var color2 = template.data.color2;
var currentColor = Session.get('currentColor');
var nextColor = currentColor === color1 ? color2 : color1;
Session.set('currentColor', nextColor);
}
});
Finally, we'll give the template access to the current color.
Template.colorSquare.helpers({
currentColor: function() {
return Session.get('currentColor');
}
});
In case you are following along at home, here is the CSS:
.color-square {
height: 25px;
width: 25px;
margin: 50px;
}
You should be able to click on the square and watch it swap colors. Awesome. Now suppose you need two of these on the page at the same time.
Two squares are a problem
<body>
{{> colorSquare color1='green' color2='purple'}}
{{> colorSquare color1='red' color2='blue'}}
</body>
You'll notice that both of the squares have the same color. This is because currentColor
will be set to color1
from the last colorSquare
. Clicking on either of the squares will cause both to change color!
We need a way for each colorSquare
to have a reactive state scoped to its template instance. Fortunately with meteor 0.9.1 we have a way to do exactly that.
Scoped reactivity to the rescue
Let's rewrite the onCreated
callback to stop using a session variable. Instead we'll store the template state in a ReactiveVar. This lets us isolate reactivity to a single local variable rather than using a shared global object.
Template.colorSquare.onCreated(function() {
this.currentColor = new ReactiveVar;
this.currentColor.set(this.data.color1);
});
We create a new ReactiveVar
instance called currentColor
and store it in the template instance. Just as we did before, we set the state to the first color. Note that for this to work you'll have to add the reactive-var package to your app.
> meteor add reactive-var
Next we'll swap out the references to Session
with currentColor
in the event callback, but otherwise leave the logic untouched.
Template.colorSquare.events({
click: function(e, template) {
var color1 = template.data.color1;
var color2 = template.data.color2;
var currentColor = template.currentColor.get();
var nextColor = currentColor === color1 ? color2 : color1;
template.currentColor.set(nextColor);
}
});
Lastly, we'll access the template instance inside of the currentColor
helper using the new Template.instance()
method. This call was unavailable in previous versions of meteor, and it forms the basis for this technique.
Template.colorSquare.helpers({
currentColor: function() {
return Template.instance().currentColor.get();
}
});
And with that, we have two templates which maintain their own private reactive state. If you'd like to learn how templates can directly access their instance variables, check out this article.