Meteor Capture

A look at local template state

Tying state to a template instance

A little look at Session variables and template state in Meteor.

Components have been on the horizon for Blaze for quite some time and one of the features that it will bring is template local reactive state. In this post we'll look at the pitfalls we can encounter using Session and how local reactive state solves these problems. As well as a solution we can implement now by using the core packages reactive-var and reactive-dict.

To illustrate, let's start by taking a look at a fresh Meteor project.

$ meteor create test

test.css  
test.html  
test.js  

Javascript

if (Meteor.isClient) {  
  // counter starts at 0
  Session.setDefault("counter", 0);

  Template.hello.helpers({
    counter: function () {
      return Session.get("counter");
    }
  });

  Template.hello.events({
    'click button': function () {
      // increment the counter when button is clicked
      Session.set("counter", Session.get("counter") + 1);
    }
  });
}

Templates

<body>  
  <h1>Welcome to Meteor!</h1>

  {{> hello}}
</body>

<template name="hello">  
  <button>Click Me</button>
  <p>You've pressed the button {{counter}} times.</p>
</template>  

What's the problem?

So you'll notice here that it's using a Session variable in order to store the counter. Now let's imagine we would like to reuse our hello template somewhere else in our application (or even on the same view).

<body>  
  <h1>Welcome to Meteor!</h1>
  {{> hello}}
  {{> hello}}
</body>  

Now unless you wish for these two distinct instances of hello to share the same counter value, you'll quickly realise that using a Session variable in this way is a bit of a problem. See screenshot below.

The usual way we get around this

As you may have seen before in the leaderboard example when we're displaying items that come from the database we can make use of the individual document _id to identify each template instance and (in this case) highlight the selected item. Here's an example of that (from the leaderboard example):

{{#each players}}
  {{> player}}
{{/each}}
if (Meteor.isClient) {  
  Template.player.helpers({
    selected: function () {
      return Session.equals("selectedPlayer", this._id) ? "selected" : '';
    }
  });

  Template.player.events({
    'click': function () {
      Session.set("selectedPlayer", this._id);
    }
  });
}

But wait... What if we wanted to display two leaderboards side by side? You may have guessed it, both lists would share the same selected item, regardless of which list you had clicked on! (that's probably not so great).

So what we're really wanting to do is tie state to an individual instance of a template.

One solution

We'll make use of both ReactiveVar and ReactiveDict in these examples.

To keep things simple to understand, let's convert the default Meteor app (shown above).

ReactiveVar example

$ meteor add reactive-var

ReactiveVar (as the name suggests) is a lightweight core package that basically lets you get and set a reactive variable.

if (Meteor.isClient) {  
  Template.hello.created = function () {
    // counter starts at 0
    this.counter = new ReactiveVar(0);
  };

  Template.hello.helpers({
    counter: function () {
      return Template.instance().counter.get();
    }
  });

  Template.hello.events({
    'click button': function (event, template) {
      // increment the counter when button is clicked
      template.counter.set(template.counter.get() + 1);
    }
  });
}
ReactiveDict example

$ meteor add reactive-dict

Here's the same code as above, but using ReactiveDict. It shares the same syntax as Session because Session is simply a ReactiveDict whose contents are preserved on hot code push.

if (Meteor.isClient) {  
  Template.hello.created = function () {
    // counter starts at 0
    this.state = new ReactiveDict();
    this.state.set('counter', 0);
  };

  Template.hello.helpers({
    counter: function () {
      return Template.instance().state.get('counter');
    }
  });

  Template.hello.events({
    'click button': function (event, template) {
      // increment the counter when button is clicked
      template.state.set('counter', template.state.get('counter') + 1);
    }
  });
}

So now we can include our hello template any number of times and each instance of the template will have its own unique counter. What's great about this is that we can create reusable components.

One important downside with this approach is that unlike Session, when your application reloads (on save or deploy) the values won't be restored. Though in many cases this is not too much of a problem. Hopefully in the future with Blaze components it will be possible to restore state after HCR.

Reference

Future work on Blaze
https://github.com/meteor/meteor/blob/devel/packages/blaze/README.md#future-work

Reactive Variable
https://atmospherejs.com/meteor/reactive-var

Reactive Dictionary
https://atmospherejs.com/meteor/reactive-dict

Tracker
https://www.meteor.com/tracker

comments powered by Disqus