I have an “old-style” currentUser-Service, which loads a current user after a successful login. Then there’s an index-route, which renders the programs of the user. Because the user-load is async, it is possible, that the index-controller’s getter programs is called before CurrentUser::user is set. If that’s the case, the index-template is not rerendered. Can somebody explain why? The user-prop is set with Ember’s set-method and therefore should be tracked. Am I right? Here’s the failing code:
// services/current-user.js
import Service, { inject as service } from '@ember/service';
import RSVP from 'rsvp';
import config from 'ember-get-config';
import { isPresent } from '@ember/utils';
export default Service.extend({
session: service('session'),
load() {
if (this.get('session.isAuthenticated')) {
let { token } = this.get('session.data.authenticated');
return fetch(`/users?me=true`, { headers: { 'Authorization': token } })
.then(resp => resp.json())
.then((user) => {
this.set('user', user); // user-property of CurrentUser-service get set with set-method
});
}
else {
return RSVP.resolve();
}
}
});
// controllers/index.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
export default class IndexController extends Controller {
@service currentUser;
get programs() {
if (!isPresent(this.currentUser.user)) {
return [];
}
return this.currentUser.user.programs.filter((program) => {
return program.key !== 'test';
});
}
}
// templates/index.js
{{#each this.programs as |program|}}
<div class="ui link card fluid">
<div class="content">
<i class="grey right floated {{program.icon}} icon"></i>
<div class="header">{{program.title}}</div>
</div>
</div>
{{/each}}
If I create an Octane-version of the currentUser-service, it works:
import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import RSVP from 'rsvp';
import config from 'ember-get-config';
import { isPresent } from '@ember/utils';
export default class CuserService extends Service {
@service session;
@tracked user;
load() {
if (this.session.isAuthenticated) {
let { token } = this.session.data.authenticated;
return fetch(`/users?me=true`, { headers: { 'Authorization': token } })
.then(resp => resp.json())
.then((user) => {
this.user = user;
});
}
else {
return RSVP.resolve();
}
}
}
The url in the Octane version has ‘essenceprogram’ prefixed to it?
Where is load() actually called from? Is it from the route model hook?
Normally with this sort of thing I’d run it in a dedicated route like the a user route, then the user would be in the model and the route’s template would be waiting for the promise to resolve before rendering, rather than running one, then changing route and hoping something notices you changing the user prop in amongst it and hoping for it to render again.
import ESASessionService from 'ember-simple-auth/services/session'
import { inject as service } from '@ember/service'
export default class SessionService extends ESASessionService {
@service currentUser
handleAuthentication() {
super.handleAuthentication('index')
this.currentUser.load();
}
handleInvalidation() {
super.handleInvalidation('/#/login');
}
}
Yes, I understand, but I want to know, why it does not work with this.set? Is this a bug? It worked with a computed-prop at index-controller without problems.
I’ not sure to be honest but I have seen the odd unexpected thing of my own when mixing up the old style with the new. Sorry I can’t pin it down exactly for you.
When the service is being referenced in the controller as this.currentUser.user.programs when normally on an old style service it would be this.currentUser.get(‘user.programs’) but yet the “get” (ie on the octane-style computed property) probably won’t know there’s a dependency on that property in that case. Maybe it’s just a fundamental incompatibility?
The rule is, if you are using set to update a value, you must use get to access it on the other side.
The native getter in your code has no way of knowing what values you are accessing. It’s completely opaque to Ember, so when it goes to render {{this.programs}}, it does not know what that code does at all. From Ember’s perspective, that’s a plain JS property - it could be tracked, it could be a getter, it has no idea.
If you render {{this.currentUser.user}} directly, Ember itself is calling get(this, 'currentUser.user'); under the hood. That’s why directly referencing a value works.
If you want to reference a classic value updated with set in a native getter, you can use Ember.get directly to do so:
// controllers/index.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { get } from '@ember/object';
export default class IndexController extends Controller {
@service currentUser;
get programs() {
let user = get(this, 'currentUser.user');
if (!isPresent(user)) {
return [];
}
return user.programs.filter((program) => {
return program.key !== 'test';
});
}
}
Long term of course, you’ll want to update the service to make user a tracked property. Then you’ll be able to access it without Ember.get.