Template onCreated runs multiple times

Hi Meteor community. I’m new to Meteor and JavaScript and guess I have some troubles working with templates. I’m storing tweets in a collection. When the page is loaded I want to load a random tweet once and make its text available by a template helper. This is the code I currently have:

main.html:

<head>
  ...
</head>

<body>
  ...
  {{> tweet}}
  ...
</body>

<template name="tweet">
  <h1 class="mt-5">{{text}}</h1>
</template>

main.js:

import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';

import './main.html';

Tweets = new Mongo.Collection('tweets');

Template.tweet.onCreated(function tweetOnCreated() {
  this.tweet = new ReactiveVar();
  this.autorun(() => {
    var docs = Tweets.find().fetch();
    var ri = Math.floor(Math.random() * docs.length);
    var doc = docs[ri];
    if (doc) {
      this.tweet.set(doc);
      console.log(doc);
    }
  });
});

Template.tweet.helpers({
  text() {
    return Template.instance().tweet.get().text;
  },
});

However, I get an exception and usually multiple calls to console.log().

Exception in template helper: text@http://localhost:3000/app/app.js?hash=aac71bf47644c725e86232955c68dc5b0b45af5f:88:43 (meteor.js, line 1048)
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3051:21
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1715:21
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3103:71
_withTemplateInstanceFunc@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3769:18
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3102:52
call@http://localhost:3000/packages/spacebars.js?hash=6f2be25813c793c0b363a6a91ebb029723f294ec:172:23
mustacheImpl@http://localhost:3000/packages/spacebars.js?hash=6f2be25813c793c0b363a6a91ebb029723f294ec:106:30
mustache@http://localhost:3000/packages/spacebars.js?hash=6f2be25813c793c0b363a6a91ebb029723f294ec:110:44
http://localhost:3000/app/app.js?hash=aac71bf47644c725e86232955c68dc5b0b45af5f:40:30
doRender@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2086:32
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1934:24
_withTemplateInstanceFunc@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3769:18
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1932:54
_withCurrentView@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2271:16
lookup:text:materialize@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1931:34
_compute@http://localhost:3000/packages/tracker.js?hash=a9bdf2f732520305683f894b4b4d4d49d90cbe05:332:38
Computation@http://localhost:3000/packages/tracker.js?hash=a9bdf2f732520305683f894b4b4d4d49d90cbe05:229:20
autorun@http://localhost:3000/packages/tracker.js?hash=a9bdf2f732520305683f894b4b4d4d49d90cbe05:602:34
autorun@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1944:29
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2080:17
nonreactive@http://localhost:3000/packages/tracker.js?hash=a9bdf2f732520305683f894b4b4d4d49d90cbe05:626:13
_materializeView@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2079:22
materializeDOMInner@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1532:31
_materializeDOM@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1474:22
[native code]
_materializeDOM@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:1483:11
http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2113:46
nonreactive@http://localhost:3000/packages/tracker.js?hash=a9bdf2f732520305683f894b4b4d4d49d90cbe05:626:13
_materializeView@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2079:22
render@http://localhost:3000/packages/blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2370:25
renderToDocument@http://localhost:3000/packages/templating-runtime.js?hash=9b008466a63cfaa8a7f0a8703fb4a6d8d253fb06:101:26
maybeReady@http://localhost:3000/packages/meteor.js?hash=33066830ab46d87e2b249d2780805545e40ce9ba:927:28
loadingCompleted@http://localhost:3000/packages/meteor.js?hash=33066830ab46d87e2b249d2780805545e40ce9ba:939:15

What am I doing wrong?

The reason you’re getting an error is in your helper

When it first runs, Template.instance().tweet.get() returns undefined, you then try to read the property text from undefined which is a TypeError.

What you want to do is check if there was anything in the ReactiveVar before reading text out:

Template.tweet.helpers({
  text() {
    const tweet = Template.instance().tweet.get();
    if (tweet) return tweet.text;
  },
});

Later on, the data is sent from the server to the client and Tweets.find() will return the tweets and the autorun will set the ReactiveVar.
That change in value will trigger the helper to re-run as it depends on the value of the ReactiveVar, and the tweet will be returned to the template and rendered.

Note that because Tweets.find is already reactive, you can skip both the autorun and the ReactiveVar, by putting that code directly in the helper:

Template.tweet.helpers({
  text() {
      var docs = Tweets.find().fetch();
      var ri = Math.floor(Math.random() * docs.length);
      var doc = docs[ri];
      if (doc) return doc
  },
});

Then you don’t need any of the code in onCreated.

Of course, with the random tweet being selected by the helper (or in your original autorun), any change to any tweet or any new tweet added to that collection will cause the function to re-run and select a new random tweet, so I haven’t actually solved your problem, just given you a new one

The simplest solution to only load only a single tweet, is just to check if we’ve already saved one:

Template.tweet.helpers({
    text() {
        const inst = Template.instance();
        if (!inst.selectedTweet) {
            var docs = Tweets.find().fetch();
            var ri = Math.floor(Math.random() * docs.length);
            var doc = docs[ri];
            inst.selectedTweet = doc;
        }
        if (inst.selectedTweet) return inst.selectedTweet.text;
    },
});

On first run:

  • inst.selectedTweet will be undefined, so it will try to get one at random.
  • Running Tweets.find will register the Tweets collection as a dependency meaning changes will cause a re-run.
  • Since the data won’t be ready yet, that will return undefined and selectedTweet will be set to undefined.

Once the data is ready:

  • The Tweets collection notifies reactive functions that it has changed
  • The helper re-runs
  • inst.selectedTweet will be undefined, so it will try to get one at random.
  • This time it will succeed, be returned and rendered.

On the first change to any Tweet:

  • The Tweets collection notifies reactive functions that it has changed
  • inst.selectedTweet will be truthy and will skip selecting a new tweet.
  • Because Tweets.find does not run this time, it’s no longer considered a dependency of this helper and it will stop listening to updates.

Sorry for the long explanation but hopefully this gives you a better understanding of Meteor’s reactivity system and how to work with it :slight_smile:

3 Likes