This post covers an example of migrating a computed property to a native class getter and addresses a few questions that I had along the way.
Let's say we have the following controller as a classic class:
export default Controller.extend({
firstName: 'David',
lastName: 'Tang',
fullName: computed('firstName', 'lastName', function () {
return `${this.firstName} ${this.lastName}`;
}),
actions: {
updateFirstAndLastName() {
this.setProperties({
firstName: 'James',
lastName: 'Aspey',
});
},
},
});
We can run ember-native-class-codemod to convert this classic class to a native class and get something like the following:
import Controller from '@ember/controller';
import { action, computed } from '@ember/object';
export default class ApplicationController extends Controller {
firstName = 'David';
lastName = 'Tang';
@computed('firstName', 'lastName')
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@action
updateFirstAndLastName() {
this.setProperties({
firstName: 'James',
lastName: 'Aspey',
});
}
}
When we invoke the updateFirstAndLastName
action, fullName
recomputes.
Q: Do we need the @computed
decorator?
When I first saw this, I wasn't sure how @computed
worked. Would fullName
recompute because it is now a getter? Let's remove it and see what happens.
import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class ApplicationController extends Controller {
firstName = 'David';
lastName = 'Tang';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@action
updateFirstAndLastName() {
this.setProperties({
firstName: 'James',
lastName: 'Aspey',
});
}
}
When we invoke the updateFirstAndLastName
action, fullName
doesn't recompute. Looks like we need the @computed
decorator.
In order to remove the @computed
decorator on fullName
, we need to change firstName
and lastName
to be tracked properties via the @tracked
decorator as follows:
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class ApplicationController extends Controller {
@tracked firstName = 'David';
@tracked lastName = 'Tang';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@action
updateFirstAndLastName() {
this.setProperties({
firstName: 'James',
lastName: 'Aspey',
});
}
}
Now because we've changed firstName
and lastName
to be tracked properties, we no longer need set()
or setProperties()
. We can update firstName
and lastName
in our updateFirstAndLastName
action using standard JavaScript assignment as shown below:
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class ApplicationController extends Controller {
@tracked firstName = 'David';
@tracked lastName = 'Tang';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@action
updateFirstAndLastName() {
this.firstName = 'James';
this.lastName = 'Aspey';
}
}
Q: Will Getters Recompute When Dependent Getters Change?
We can also create getters that depend on other getters, and they'll recompute whenever the dependent getters change. For example, let's create a fullNameUpperCased
getter that reads fullName
.
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class ApplicationController extends Controller {
@tracked firstName = 'David';
@tracked lastName = 'Tang';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
get fullNameUpperCased() {
return this.fullName.toUpperCase();
}
@action
updateFirstAndLastName() {
this.firstName = 'James';
this.lastName = 'Aspey';
}
}
When we invoke the updateFirstAndLastName
action, the fullNameUpperCased
getter recomputes as fullName
recomputes.
Q: Do We Need the @computed
Decorator If We Are Relying on a Computed Property?
Let's say we have the following:
import Controller from '@ember/controller';
import { action, computed } from '@ember/object';
export default class ApplicationController extends Controller {
firstName = 'David';
lastName = 'Tang';
@computed('firstName', 'lastName')
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@computed('fullName')
get fullNameUpperCased() {
return this.fullName.toUpperCase();
}
@action
updateFirstAndLastName() {
this.setProperties({
firstName: 'James',
lastName: 'Aspey',
});
}
}
Do we need @computed('fullName')
on the fullNameUpperCased
getter? It turns out we don't. In the code below, fullNameUpperCased
will recompute when fullName
recomputes.
import Controller from '@ember/controller';
import { action, computed } from '@ember/object';
export default class ApplicationController extends Controller {
firstName = 'David';
lastName = 'Tang';
@computed('firstName', 'lastName')
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
get fullNameUpperCased() {
return this.fullName.toUpperCase();
}
@action
updateFirstAndLastName() {
this.setProperties({
firstName: 'James',
lastName: 'Aspey',
});
}
}
Q: Can Computed Properties Rely on Getters?
Computed properties can't rely on getters. For example, fullNameUpperCased
can't be a computed property that depends on the fullName
getter. The following won't work:
import Controller from '@ember/controller';
import { action, computed } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class ApplicationController extends Controller {
@tracked firstName = 'David';
@tracked lastName = 'Tang';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@computed('fullName')
get fullNameUpperCased() {
return this.fullName.toUpperCase();
}
@action
updateFirstAndLastName() {
this.firstName = 'James';
this.lastName = 'Aspey';
}
}
When we invoke the updateFirstAndLastName
action, fullName
will recompute but fullNameUpperCased
won't.