2

I'm trying to wrap my head around Meteor's reactivity. I understand it re-renders a page when a reactive data source that's being referenced in a template changes. I also understand what constitutes a reactive source (Session, MongoDB cursors, etc.).

What I'm having trouble understanding are all these "behind my back" calls to my template helpers. There seems to be something more than reactivity that causes them.

In particular, in the following code, I have a friendRequests helper that gets recomputed sometimes two sometimes three times during a single access to the /friends page. If it gets recomputed twice, the DB queries succeed! If it gets recomputed thrice, the first DB access (for some odd reason) fails to query the DB while the latter two succeed.

This is the stack trace when the DB fails:

// NOTE: imsolonely is one of the users that should be returned in the friendRequests
imsolonely's public key: undefined
debug.js:41 Exception in template helper: TypeError: Cannot read property 'profile' of undefined
    at Object.Utils.getPublicKeyByUsername (http://localhost:3000/lib/utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:79:22)
    at Object.Utils.getFingerprintByUsername (http://localhost:3000/lib/utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:88:24)
    at http://localhost:3000/client/friends.js?dbec4a7537c9d0abf56a74489824969cb7baadfe:25:35
    at Array.forEach (native)
    at Function._.each._.forEach (http://localhost:3000/packages/underscore.js?0a80a8623e1b40b5df5a05582f288ddd586eaa18:156:11)
    at Object.Template.friendRequests.helpers.friendRequests (http://localhost:3000/client/friends.js?dbec4a7537c9d0abf56a74489824969cb7baadfe:22:7)
    at http://localhost:3000/packages/blaze.js?77c0809654ee3a10dcd5a4f961fb1437e7957d33:2693:16
    at http://localhost:3000/packages/blaze.js?77c0809654ee3a10dcd5a4f961fb1437e7957d33:1602:16
    at Object.Spacebars.dot (http://localhost:3000/packages/spacebars.js?3c496d2950151d744a8574297b46d2763a123bdf:231:13)
    at http://localhost:3000/client/template.friends.js?a2da726f6dad1aaecfdedfe216aa3378fff938b5:24:37
utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:78 imsolonely's public key: {"profile":{"publicKey":"0404b4880129edc1ea2652dd1eff1c8728269874b9b0ace02cc90edcb449c3f3d716c2f8b79a5fe5695d52cd85aed228f977073538625e8e71f1cfd764766669b1"},"_id":"sXjzt7YHA8KTyAib5"}
utils.js?acf4e03d4c8a70819c26f8d2fd08caf7100768fe:78 imsolonely's public key: {"profile":{"publicKey":"0404b4880129edc1ea2652dd1eff1c8728269874b9b0ace02cc90edcb449c3f3d716c2f8b79a5fe5695d52cd85aed228f977073538625e8e71f1cfd764766669b1"},"_id":"sXjzt7YHA8KTyAib5"}

This also happens with many (or all, not sure) of my helpers. They get called when they are should not even be needed because the user is not logged in (and the template is not rendered, thus the helper should not be recomputed).

Here's some code:

client/friends.js:

Template.friendRequests.helpers({
  friendRequests: function () {
    // Problem 1: The template gets called right after I log in but before I am fully logged in
    // so I need this call here.
    if(!Meteor.user())
      return Utils.notLoggedInErrorMsg;

    // Problem 2: The template gets called three times. The first time fails the DB query
    // as if the DB row did not exist. The next 2 times it succeeds. But it should only be
    // called once.
    var reqs = Friends.getFriendRequests(Utils.me());

    _.each(reqs, function(element, it, list) {
      check(element.initiator, String);
      // Get the user's fingerprint
      element.fingerprint = Utils.getFingerprintByUsername(element.initiator); 
    });

    return reqs;
  },  
});

client/friends.html:

<template name="friends">
  {{> friendRequests}}
  {{> searchForm}}

  <h2>My friends</h2>
  <ul>
  {{#each friends}}
    <li>{{this}}</li>
  {{/each}}
  </ul>
</template>

<template name="friendRequests">
  <h2>Friend requests</h2>
  {{#if friendRequests.length}}
    <p>Someone's popular today!</p>
    <ul>
    {{#each friendRequests}}
      <li><b>{{initiator}}</b> with fingerprint <pre style="display: inline">{{fingerprint}}</pre> sent   you a request on <em>{{date}}</em>. <a href="#">Accept <b>{{initiator}}</b> as a friend?</a></li>
    {{/each}}
    </ul>
  {{else}}
    <p>Sorry, nobody likes you right now.</p>
  {{/if}}
</template>

lib/utils.js:

Utils = {
  // ...
  // other stuff
  // ...

  // @return a hex-encoded public key as a string
  getPublicKeyByUsername: function (username) {
    var user = Meteor.users.findOne({ username: username }, { fields: { 'profile.publicKey': 1 } }); 
    console.log(username + '\'s public key: ' + EJSON.stringify(user));
    var pubKey = user.profile.publicKey;

    return pubKey;
  },  

  // NOTE: not used yet, i used the CryptoUtils function directly when I needed it
  //  
  // @return the fingerprint as a hex-encoded string
  getFingerprintByUsername: function (username) {
    var pubKey = Utils.getPublicKeyByUsername(username);

    var fingerprint = CryptoUtils.getPublicKeyFingerprint(pubKey);

    return fingerprint;
  },  

  notLoggedInErrorMsg: 'Meteor is being silly and calling this when I\'m not logged in.',
}

If it matters, I am using the iron:router 1.0.3 package for redirecting to the /friends URL.

Any clarifications as to why the friendRequests helper is being recomputed when it's out of view, why it's being recomputed sometimes twice, sometimes thrice, instead of once when I refresh the /friends page would be greatly appreciated!

Thank you, Alin

Alin Tomescu
  • 353
  • 1
  • 4
  • 20

2 Answers2

4

In general you should expect that your helpers will get called many times. If your template is being rendered while the user is logging in, and without waiting on the data to be published, it's likely that your helper will run:

  1. When the route is requested
  2. When the user finishes logging in
  3. When Friends data first arrives on the client
  4. When additional Friends data arrives or is modified

To solve the first problem, you can check for Meteor.user() || Meteor.loggingIn() as I do in the answer to this question (note the router API has changed since I answered that but it should give you an idea of what to do).

The answer to your second issue can be found in my post on guards. In short you can't assume that the data has arrived on the client, so you either need to check for its existence or you need to explicitly wait on the subscription in your router prior to rendering the template.

Community
  • 1
  • 1
David Weldon
  • 59,944
  • 10
  • 136
  • 139
  • So I do have a `Router.configure()` that sets the default layout and has a `waitOn` for all the `Meteor.subscribe` calls needed in **all** of my code. I also have a `Router.onBeforeAction` that redirects to a login page with a *different layout*. I really dislike that I have to check in tens of templates for `Meteor.user()` when those templates are **not even rendered** (or at least should not be rendered) when `Meteor.user() == false`. – Alin Tomescu Dec 02 '14 at 20:27
  • Plus, even if I were to check everywhere, what would I return? I'm guessing null is a bad idea. So do I construct & return dummy objects of that helper's type for each one of my template helpers? Isn't there a better way of doing this? – Alin Tomescu Dec 02 '14 at 20:27
  • Also, the guard stuff might be acceptable in simple code like `var post = Posts.findOne(); return posts && posts.id;` However, it complicates simple/linear code that needs to process the DB data a little bit before returning it. And the programming overhead is added up for each helper you write and for each collection you query in that helper. I was hoping there's a Meteor-specific way to ensure that within certain helpers you have: **1.** `Meteor.user()` is always the logged in user's ID. **2.** DB queries should not fail for no-good reasons while the data is there and the query is correct. – Alin Tomescu Dec 02 '14 at 20:35
  • You shouldn't have to check every route for the user. You can apply the same hook to all of them. In our app we have a route controller which renders a connecting template when the user is logging in, and otherwise renders the correct template. All other route controllers extend from this one. Your db calls should never fail if the data is indeed on the client. Perhaps your waitOn hook isn't implemented correctly. You'll find that guards are often necessary when dealing with reactive data - it's just the reality of using this framework. – David Weldon Dec 02 '14 at 20:57
2

It turns out there was an error in my waitOn hook. I was not returning anything, I was just making Meteor.subscribe() calls instead of returning an array of them. Here's what fixed the DB exceptions:

Router.configure({
  layoutTemplate: 'mainLayout',
  loadingTemplate: 'loading',
  waitOn: function () {
    return [
      Meteor.subscribe('userData'),
      Meteor.subscribe('allUserData'),
      Meteor.subscribe('UserProfiles'),
    ];
  },
});

I still don't have a good solution for the Meteor.user() being undefined. Or maybe this fixed it as well?

PS: Thank you @DavidWeldon for helping me narrow this down!

Alin Tomescu
  • 353
  • 1
  • 4
  • 20