Migrating Away from Computed Properties in Ember

Last reviewed on November 13, 2020

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.

Try it here

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.

Try it here

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',
    });
  }
}

Try it here

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';
  }
}

Try it here

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.

Try it here

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',
    });
  }
}

Try it here

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.

Try it here