Previous: Writing Code Without Tasks
Next: Refactoring With Tasks
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); } } } }
This is not the beautiful Ember code we all thought we'd be writing, and unfortunately this kind of code is extremely commonplace.
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.
Previous: Writing Code Without Tasks
Next: Refactoring With Tasks