A Quick Post-Mortem

In the previous part of the tutorial, we built a component that fetches and displays nearby retail stores. As you can see, it takes quite a bit of code to cover all of the corner cases and build something that is actually production-ready:

import { action } from '@ember/object';

export default class Tutorial5 extends TutorialComponent {
  result = null;
  isFindingStores = false;

  @action
  async findStores() {
    if (this.isFindingStores) { return; }

    let geolocation = this.geolocation;
    let store = this.store;

    this.set('isFindingStores', true);

    try {
      let coords = await geolocation.getCoords()
      let result = await store.getNearbyStores(coords);

      if (this.isDestroyed) { return; }

      this.set('result', result);
    } finally {
      if (!this.isDestroyed) {
        this.set('isFindingStores', false);
      }
    }
  }
}
Toggle JS / Template
Example

This is not the beautiful Ember code we all thought we'd be writing, and unfortunately this kind of code is extremely commonplace.

Alternative: Move tricky code to an object with a long lifespan

Components have limited lifespans: they're rendered, and then eventually they're unrendered and destroyed. Controllers, Services, Ember-Data Stores, and Routes, on the other hand, live forever (or at least until the app is torn down in a testing environment).

As such, one approach to avoiding "set on destroyed object" errors is to move tricky async logic into a method/action on a Controller or Service that is invoked by a Component. Sometimes this works, but it's often the case that even though you no longer see exceptions in the console, you still need to clean up / stop / cancel some operation on a long lived object in response to a Component being destroyed. There are Component lifecycle hooks like willDestroyElement or element modifiers like will-destroy that you can use for these kinds of things, but then you still end up with the same amount of code, but now it's smeared between Component and Controller.