Ayrton Senna
Ayrton Senna

Reputation: 3835

Meteor Iron router hooks being run multiple times

EDIT: Here is the github repo. And you can test the site here.

On the homepage, just open the browser console and you will notice that WaitOn and data are being run twice. When there is no WaitOn, then the data just runs once.


I have setup my pages by extending RouteController and further extending these controllers. For example:

    ProfileController = RouteController.extend({
        layoutTemplate: 'UserProfileLayout',
        yieldTemplates: {
            'navBarMain': {to: 'navBarMain'},
            'userNav': {to: 'topUserNav'},
            'profileNav': {to: 'sideProfileNav'}
        },
        // Authentication
        onBeforeAction: function() {
            if(_.isNull(Meteor.user())){
              Router.go(Router.path('login'));
            } else {
                this.next();
            } 
          }
     });

ProfileVerificationsController = ProfileController.extend({
    waitOn: function() {
        console.log("from controller waitOn");
        return Meteor.subscribe('userProfileVerification');
    },

    data: function() {
        // If current user has verified email
        console.log("from controller data start");
        var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
        var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';

        var data = {
            verifiedEmail: verifiedEmail,
            verifiedPhoneNumber: verifiedPhoneNumber
        };
        console.log("from controller data end");
        return data;
    }
});

On observing the console in the client, it seems the hooks are being run 2-3 times. And I also get an error on one of the times because the data is not available. The following is the console on just requesting the page once:

from controller waitOn
profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:44 from controller data start
debug.js:41 Exception from Tracker recompute function: Cannot read property 'phoneNumber' of undefined
TypeError: Cannot read property 'phoneNumber' of undefined
    at ProfileController.extend.data (http://localhost:3000/lib/router/profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:46:62)
    at bindData [as _data] (http://localhost:3000/packages/iron_controller.js?b02790701804563eafedb2e68c602154983ade06:226:50)
    at DynamicTemplate.data (http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:219:50)
    at http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:252:25
    at null.<anonymous> (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2445:26)
    at http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1808:16
    at Object.Blaze._withCurrentView (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2043:12)
    at viewAutorun (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1807:18)
    at Tracker.Computation._compute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:296:36)
    at Tracker.Computation._recompute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:310:14)
from controller data start
from controller data end
from controller waitOn
from controller data start
from controller data end

Have I not used the controllers properly?

Upvotes: 2

Views: 988

Answers (1)

Keith Dawson
Keith Dawson

Reputation: 1495

Without being able to see the rest of the code that you have defined that uses these route controllers (such as templates or route definitions), I cannot accurately speak to the reason for the data function being called multiple times. I suspect that you may be using the ProfileVerificationsController with multiple routes, in which case the data definition for this controller would be executed multiple times, one for each route that uses the controller. Since the data definition is reactive, as you browse through your application and data changes, this might be resulting in the code defined to be rerun.

As for your controller definitions, I would suggest making a few modifications to make the code more robust and bulletproof. First, the ProfileController definition:

    ProfileController = RouteController.extend({
        layoutTemplate: 'UserProfileLayout',
        yieldRegions: {
            'navBarMain': {to: 'navBarMain'},
            'userNav': {to: 'topUserNav'},
            'profileNav': {to: 'sideProfileNav'}
        },
        onBeforeAction: function() {
            if(!Meteor.user()) {
                Router.go(Router.path('login'));
                this.redirect('login'); // Could do this as well
                this.render('login'); // And possibly this is necessary
            } else {
                this.next();
            }
        }
    });

Notice the first thing that I changed, yieldTemplates to yieldRegions. This typo would prevent the regions from your templates using this route controller to be properly filled with the desired subtemplates. Second, in the onBeforeAction definition, I would suggest checking not only whether or not the Meteor.user() object is null using Underscore, but also checking for whether or not it is undefined as well. The modification that I made will allow you to check both states of the Meteor.user() object. Finally, not so much a typo correction as an alternative suggestion for directing the user to the login route, you could use the this.redirect() and this.render() functions instead of the Router.go() function. For additional information on all available options that can be defined for a route/route controller, check this out.

Now for the ProfileVerificationsController definition:

    ProfileVerificationsController = ProfileController.extend({
        waitOn: function() {
            return Meteor.subscribe('userProfileVerification');
        },
        data: function() {
            if(this.ready()) {
                var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
                var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';

                var data = {
                    verifiedEmail: verifiedEmail,
                    verifiedPhoneNumber: verifiedPhoneNumber
                };
                return data;
            }
        }
    });

Notice the one thing that I changed, which is to wrap all of your code defined in the data option for your controller with a if(this.ready()){}. This is critical when using the waitOn option because the waitOn option adds one or more subscription handles to a wait list for the route and the this.ready() check returns true only when all of the handles in the wait list are ready. Making sure to use this check will prevent any cases of data unexpectedly not being loaded yet when you are building up your data context for the route. For additional information on defining subscriptions for your routes/route controllers, check this out.

As a final suggestion, for your onBeforeAction option definition in your ProfileController, I would suggest moving this out into its own global hook like so:

    Router.onBeforeAction(function() {
        if(!Meteor.user()) {
            Router.go(Router.path('login'));
        } else {
            this.next();
        }
    });

Defining this check in the global hook ensures that you don't have to worry about adding your ProfileController to all of your routes just to make sure that this check is run for all of them. The check will be run for every route every time that one is accessed. Just a suggestion, though, as you may have reasons for not doing this. I just wanted to suggest it since I make sure to do it for every Meteor app that I develop for additional security.

Upvotes: 1

Related Questions