Charles Williams
Charles Williams

Reputation: 573

Ember.js Computed Property vs Observer vs didReceiveAttrs() in a Component

I have created a top navbar in my ember.js application and converted it to a component so that I can have a simple "Welcome {{currentUser.fullName}}" introduction once a user signs in. There are some other reasons why the navbar is going to be a component, but for now this is what I am trying to get working correctly. We are also using ember-simple-auth addon and I am using the session.isAuthenticated value to make this happen.

Of course I have a 'login' route, that when the user signs in, they are transitioned to the 'index'. For some reason it doesn't seem like the session.isAuthenticated value is caught right away during the transition to the 'index'. I don't know why this is, but that is another issue.

Anyway, I am using a lot of components in this application and most of those components get their values from the sessions and querying the store independently of the routes so that I can use these components on multiple routes. The navbar is no exception. Right now I have the following working code:

export default Ember.Component.extend({
    layout,
    session: Ember.inject.service(),
    currentUser: Ember.inject.service(),
    isAuthenticated: Ember.observer('session.isAuthenticated', function() {
        this.didReceiveAttrs();
    }),
    currentUserFullName: null,
    user: null,
    tagName: 'navbar-main',
    className: ['navbar', 'main'],
    didReceiveAttrs() {
        this._super(...arguments);

        if (this.get('session.isAuthenticated')) {
            this._setCurrentUser();
        }
    },
    _setCurrentUser() {
        this.get('currentUser').load().then(user => this.set('user', user));
    },
    onGetCurrentUser: Ember.observer('user', function() {
        this.getUser();
    }),
    getUser: function() {
        var user = this.get('user');

        if(user) {
            this.set('user', user);
            this.set('currentUserFullName', user.get('fullName'));
        } else {
            user = this.get('store').findRecord('user', 'me');
            this.set('user', user);
            this.set('currentUserFullName', user.get('fullName'));
        }
    }
});

Everything I am reading suggests to just use didReceiveAttrs() instead of an observer. Also, it says that computed properties are usually what you want to use over observers. However I can't seem to get anything to work without the following:

isAuthenticated: Ember.observer('session.isAuthenticated', function() {
    this.didReceiveAttrs();
}),

If I try to convert it to a computed property it doesn't seem to do anything on a property change unless I refresh the page after it has already transitioned to the 'index' route. If I try to use just the didReceiveAttrs() method with this.get('session.isAuthenticated') logic, a page refresh is still needed.

So what am I not understanding about how to use Ember.computedand didReceiveAttrs()? Can someone give me an example? Or show me a more efficient way to do what I am trying to accomplish?

UPDATED CODE:

// app/pods/components/navbar-main/component.js
import Ember from 'ember';
import layout from './template'; 

export default Ember.Component.extend({
    layout,
    session: Ember.inject.service(),
    currentUser: Ember.inject.service(),
    user: null,
    isAuthenticated: Ember.observer('session.isAuthenticated', function() {
        this.get('currentUser').load().then(user => this.set('user', user));
    }),
    currentUserFullName: Ember.computed.alias('user.fullName'),
    tagName: 'navbar-main',
    className: ['navbar', 'main'],
    didReceiveAttrs() {
        this._super(...arguments);

        if(this.get('session.isAuthenticated')) {
            this.isAuthenticated();
        }
    }
});

<!-- app/pods/components/navbar-main/template.hbs -->
{{#if session.isAuthenticated}}
    <div class="ui black user login label">
        <div class="ui grid">
            <div class="twelve wide column">
                <div class="detailed">{{currentUserFullName}}<br>Welcome</div>
            </div>
            <div class="four wide column" style="padding-top: 12px; margin-top: 0;">
                <img class="ui top aligned small avatar image" src="http://semantic-ui.com/images/avatar/small/christian.jpg">
                <!--<img class="ui top aligned tiny avatar image" src="{{currentUser.avatar}}">-->
            </div>
        </div>
    </div>
{{/if}}

UPDATE 2 (Final code with help from @AdamCooper and @kumkanillam):

export default Ember.Component.extend({
    layout,
    session: Ember.inject.service(),
    currentUser: Ember.inject.service(),
    user: null,
    isAuthenticated: Ember.on('init', Ember.observer('session.isAuthenticated', function() {
        this.get('currentUser').load().then(user => this.set('user', user));
    })),
    currentUserFullName: Ember.computed.alias('user.fullName'),
    tagName: 'navbar-main',
    className: ['navbar', 'main']
});

So this is the least amount of code, still using observer and computed methods, but not using the component lifecycle. So I guess it is still necessary to use observers sometimes is what I have come up with.

Upvotes: 2

Views: 3759

Answers (3)

Charles Williams
Charles Williams

Reputation: 573

This is what I ended up with that worked for my situation:

export default Ember.Component.extend({
    layout,
    session: Ember.inject.service(),
    currentUser: Ember.inject.service(),
    user: null,
    isAuthenticated: Ember.on('init', Ember.observer('session.isAuthenticated', function() {
        this.get('currentUser').load().then(user => this.set('user', user));
    })),
    currentUserFullName: Ember.computed.alias('user.fullName'),
    tagName: 'navbar-main',
    className: ['navbar', 'main']
});

This is using a combination of Ember Freak's and Adam Cooper's answers to this question.

Upvotes: 0

Adam Cooper
Adam Cooper

Reputation: 8677

As a general rule you want to avoid the use of observers whenever possible. Steffan Penner has a great video on observers that I would recommend you watch to truly understand why they are potentially hazardous to your codebase.

In your case the load() function is returning a promise which is not something you want to return from a computed property so you will need to use an observer on isAuthenticated. I've simplified your code as much as I can without being able to run it, but hopefully you should get the idea:

export default Ember.Component.extend({
    layout,
    session: Ember.inject.service(),
    currentUser: Ember.inject.service(),

    user: null,

    authenticatedObserver: Ember.observer('session.isAuthenticated', function() {
        // Load the user, this will cause the currentUserFullName to be 
        // updated. We're using an observer here because load() returns
        // a promise 
        this.get('currentUser').load().then(user => this.set('user', user));
    }),

    // This computed property will be re-calculated whenever user is modified
    currentUserFullName: Ember.computed.alias('user.fullName')  
});

Upvotes: 3

Ember Freak
Ember Freak

Reputation: 12872

Ember will call components life cycle hook methods in the order according to rendering scenario.

On initial render - init,didReceiveAttrs,willRender,didInsertElement,didRender
On Re-Rener - didUpdateAttrs,didReceiveAttrs,willUpdate,willRender,didRender
On Component destroy - willDestroyElement,willClearRender,didDestroyElement
(Reference - https://guides.emberjs.com/v2.7.0/components/the-component-lifecycle/)

We should not call those component life cycle hook methods.

I beleive,you can completely isolate currentUser specific code to currentUser service, so that you can easily inject and use it for all your components.

export default Ember.Component.extend({
    session: Ember.inject.service(),
    currentUser: Ember.inject.service(),
    currentUserFullName: Ember.computed.alias('currentUser.currentUserFullName'),
    user: Ember.computed.alias('currentUser.user'),

    tagName: 'navbar-main',
    className: ['navbar', 'main'],    

});

Initialize currentUser and their details by observing session.isAuthenticated property.

services\current-user.js

import Ember from 'ember';

export default Ember.Service.extend({
  session: Ember.inject.service(),
  currentUserFullName: null,
  user: null,

  loadCurrentUserDetails:Ember.observer('session.isAuthenticated', function(){
    //load async data and set user and currentUserFullName.
  })
});

Upvotes: 2

Related Questions