Helper function seems to be called 3 times instead of 1 only

Hello, I want to access the current username but the helper function is called 3 times and the first time, Meteor.user() value is undefined. Why ?

input.js:

Template.input.helpers({
    categoriesNumber() {
        console.log('categoriesNumber(): '+Meteor.user());
        return Categories.find({owner: 'test'}).count();
    },
    categories() {
        console.log('categories(): '+Meteor.user());
        return Categories.find({owner: 'test'}, {sort: {createdAt: -1}});
    },
    isYourLinksList() {
        return true
        //return Meteor.user().username == FlowRouter.getParam('username');
    }
});

In the chromium console i get:

categoriesNumber(): undefined
sockjs-0.3.4.js:854 XHR finished loading: GET "http://localhost:3000...
input.js:13 categoriesNumber(): [object Object]
input.js:13 categoriesNumber(): [object Object]
input.js:17 categories(): [object Object]

I’ve made a research on the whole projet and categoriesNumber is written only one time in the input.html below:

<template name="input">
    <div id="input">
        {{#if categoriesNumber}}
            <form class="new-link" autocomplete="off">
                <div class="close" id="addLink">
                  <i class="fa fa-times" id="addLink"></i>
                </div>
                <h2>
                    {{#if isYourLinksList}}
                        Add link
                    {{else}}
                        Suggest a link
                    {{/if}}
                </h2>
                <input type="text" name="link" placeholder="http(s)://..." />
                <input type="text" name="description" placeholder="description" />
                <select name="category">
                    {{#each categories}}
                        <option value="{{_id}}">{{name}}</option>
                    {{/each}}
                </select>
                <input class="submitBtn" type="submit" value="Add" />
            </form>
        {{/if}}
        {{#if isYourLinksList}}
            <form class="new-category" autocomplete="off">
                <h2>Add category</h2>
                <span>
                    <input type="text" name="name" placeholder="category name">
                </span>
                <input class="submitBtn" type="submit" value="Add" />
            </form>
        {{/if}}
    </div>
</template>

Thank you

that’s correct.

helpers are called, whenever the data changes that you access in your helpers.

Meteor.user() is a so called reactive data-source, as is Categories.find({owner: 'test'}).count(); or any collection function

Meteor.user() is first undefined, because the user is not there yet. it will be called a second time, when the user info arrives on the client (via publish/subscribe).

The reason why it is called three times in your example is because Categories.find({owner: 'test'}).count(); is also reactive and may change over time

1 Like

Try this instead:

isYourLinksList() {
  let user = Meteor.users.findOne(Meteor.userId(), {
    fields: {
       username: 1
    }
  );
  return user && user.username === FlowRouter.getParam('username');
}

If you are willing to extend Tracker by adding Tracker.guard:

isYourLinksList() {
  return Tracker.guard(() => Meteor.user() && Meteor.user().username === FlowRouter.getParam('username'));
}

personally i would avoid adding too much non-standard apis.

Better be explicit:

isYourLinksList() {
  return Meteor.user() && Meteor.user().username == FlowRouter.getParam('username');
}

That will still be overly reactive, since it will rerun whenever any field in Meteor.user() changes. It doesn’t really matter in this case from a practical perspective, but it can matter a lot for templates that have many instantiations or expensive helper functions.

1 Like

Hi, thanks for your help.
The problem happens only on categoryNumber() actually. I’ve tried your solution using userId() (which is supposed to be also stored on client side if I well understand). The problem is still the same.

categoriesNumber() {
        console.log('categoriesNumber(), Meteor.user() = '+Meteor.user());
        let user = Meteor.users.findOne(Meteor.userId(), {
            fields: {
                username: 1
            }
        });
        console.log('categoriesNumber(), user = '+user);

        return Categories.find({owner: user.username}).count();
    },

And console result:

categoriesNumber(), Meteor.user() = undefined
categoriesNumber(), user = undefined
Exception in template helper bla bla bla...
categoriesNumber(), Meteor.user() = [object Object]
categoriesNumber(), user = [object Object]
categoriesNumber(), Meteor.user() = [object Object]
categoriesNumber(), user = [object Object]
categories(): [object Object]

I could make it working with if(Meteor.user()) but things don’t look pretty, isn’t it ?

The problem with your above code is that the first console.log is calling Meteor.user() itself which is still making the whole function overly reactive to any change in the user object. Try removing the first console.log.

See my tip here, then you can write:

categoriesNumber() {
    if (!Meteor.userId()) return 0; // to get rid of the "Exception in template helper..."
    let user = Meteor.user({username: 1});
    console.log('categoriesNumber(), user = '+user);
    return Categories.find({owner: user.username}).count();
},

Also be careful, sometimes when logging-in Meteor.userId() is set before the Meteor.user() object is fully populated by the server. Hence I would actually write:

categoriesNumber() {
    let user = Meteor.user({username: 1});
    if (!user || !user.username) return 0;
    return Categories.find({owner: user.username}).count();
},

Without my patch this would have to be:

categoriesNumber() {
    if (!Meteor.userId()) return 0;
    let user = Meteor.users.findOne(Meteor.userId(), {fields: {username: 1}});
    if (!user.username) return 0;
    return Categories.find({owner: user.username}).count();
},

Responding to your question in my other thread:

Correct, but each of those fields may be published to the client by different publish functions on the server, so the client’s copy of the user object may be updated in stages.

Also, your categoriesNumber() function also relies on the Categories collection which will be updated separately to the Users collection, resulting in at least two re-renders. This is unavoidable in this case.

Try changing

        console.log('categoriesNumber(), user = '+user);
        return Categories.find({owner: user.username}).count();

to:

        const count = Categories.find({owner: user.username}).count();
        console.log('categoriesNumber(), user = '+JSON.stringify(user), 'count='+count);
        return count;

And you will see exactly what is changing in both collections each time the categoriesNumber() function is called.

1 Like

Sorry for the question on the patch, I read and replied quickly without seeing that you actually answered on my initial thread. As you said, my purpose is indeed to “get in to good habits of making both as efficient as possible from the start” although I could just ignore it and go on.

After a deeper investigation in my project, it seems that I messed with template calls, the undefined value magically disappeared (without even using the patch):

XHR finished loading: GET "http://localhost:3000/soc [...]
input.js:14 categoriesNumber(), Meteor.user() = daMP5pPQaLjtYmvBy
input.js:22 categoriesNumber(), user = {"username":"new","_id":"daMP5pPQaLjtYmvBy"} count=0
input.js:14 categoriesNumber(), Meteor.user() = daMP5pPQaLjtYmvBy
input.js:22 categoriesNumber(), user = {"username":"new","_id":"daMP5pPQaLjtYmvBy"} count=2

with this code:

categoriesNumber() {
        console.log('categoriesNumber(), Meteor.user() = '+Meteor.userId());
        let user = Meteor.users.findOne(Meteor.userId(), {
            fields: {
                username: 1
            }
        });
        
        const count = Categories.find({owner: user.username}).count();
        console.log('categoriesNumber(), user = '+JSON.stringify(user), 'count='+count);
        return count;
},

All that’s left is to find out why I have a first count=0 before having the expected result and remove it.

Thank you for all of your explanations and suggestions.

[EDIT]
I forgot the first console.log('categoriesNumber(), Meteor.user() = '+Meteor.userId());, removing it solve the count=0 problem :face_with_hand_over_mouth:

[EDIT 2]
In fact not really… it randomly appear a count=0 before a count=X:face_with_raised_eyebrow::thinking: well I’ll investigate more, try your patch

The count=0 message is happening because your Meteor.user() object and your Categories collection are being updated at different times. This is unavoidable and you should not worry about it at all.

The categoriesNumber() function is reactive to each and every change in the user object AND the Categories collection. Here’s what’s happening in sequence:

  1. Refresh page, initially user is logged out. Meteor.userId() == null && Meteor.user() == null
  2. User is logged in using stored token, Meteor.userId() is set to id, but Meteor.user() is still null because the server hasn’t has time to send the data yet.
  3. At some point here your client code will subscribe to the Categories publication.
  4. Client finally receives user object from client. The Meteor.user() object is completed but no Categories have arrived from the server yet. The function runs again because it sees that Meteor.user().username has changed. This is when you get:
input.js:22 categoriesNumber(), user = {"username":"new","_id":"daMP5pPQaLjtYmvBy"} count=0
  1. Finally the client receives the Categories data from the client. This causes the function to run again because this time the result of Categories.find....count() has changed.

I highly recommend the Chrome Meteor DevTools extension. Using this you can watch the DDP messages arrive and see exactly what’s going on.

The performance impact from the user’s point of view will be negligible, so I really wouldn’t worry about this. Your function is reactive to the data it needs to be reactive to.

If you really wanted to prevent this type of partial rendering of the template as the data loads, then you need to conditionally render the inner template only when all the subscriptions are ready. Have a look at the docs for Meteor.subscribe() - you will see that the result is a subscription handle which has a reactive ready() function. You can use that ready() function somewhere in the outer template to only render the inner template once all the data is ready.

If you really don’t want your helpers to run before all the data is ready, you can just put everything behind an

{{#if Template.subscriptionsReady }}
// OR
{{#unless Template.subscriptionsReady }}

block (as long as the subscriptions are scoped to the template)

Template.input.onCreated(function() {
    this.subscribe('someCategoriesSubscription');
});
<template name="input">
    {{#unless Template.subscriptionsReady}}
    <p>...loading</p>
    {{else}}
    <div id="input">
        {{#if categoriesNumber}}
            <form class="new-link" autocomplete="off">
    // etc...
    {{/unless}}