Search code examples
ember.js

Ember 4.x (Octane) - replacement for Observers?


I am nearly done upgrading a very old Ember.js project from 2.13 all the way up to the latest LTS release, 4.12. Along the way I've been coming to grips with the changes that the Octane release brought about, and one area I'm struggling to adapt is our use of observers.

We use observers in two files to fire function calls when an observed value changes. I understand that Observers are discouraged in Octane, so if possible I'd like to migrate away from them.

My understanding is that tracked properties are the end-all be-all now; they're largely meant to replace computed properties, and I assume other state-based functions like observers, but I'm not really sure how to apply them in this use case, or if it's even possible.

My question is: Is there a preferred replacement for Observers in Ember Octane (4.x+)?

EDIT Here is an example of how we're using observers. It's pretty straightforward: if the marked attribute in the model changes, call the service function:

import Model, { attr } from '@ember-data/model';
import { observer } from '@ember/object';
import { service } from '@ember/service';

export default Model.extend({
  tracking: service(),

  serialNumber: attr('string'),
  ...
  serialNumberChanged: observer('serialNumber') {
    this.tracking.trackSerialNumberUpdated(this); // The service function
  }
});

Solution

  • Is there a preferred replacement for Observers in Ember Octane (4.x+)?

    Not directly. Observers are an anti pattern, generally for performance reasons, as they tend to be used as "effects", which cause extra renders and can slow down an app significantly, especially if frequently called.

    Replacing them largely depends on what they were used for.

    • In most cases the answer is move the logic to where the initial change occurred.
    • But in some cases, the answer is to lean more in to derived data.

    Response to the update:

    Given this code:

    import Model, { attr } from '@ember-data/model';
    import { observer } from '@ember/object';
    import { service } from '@ember/service';
    
    export default Model.extend({
      serialNumber: attr('string'),
      ...
      serialNumberChanged: observer('serialNumber') {
        this.tracking.trackSerialNumberUpdated(this); // The service function
      }
    });
    

    I would point out that any data layer should never reach out in to your application, and this behavior should be inverted (there are many reasons for this, which is outside the scope of this SO question!, but it's why you see in larger apps the whole data layer is a separate package -- it forces this constraint, because packages/libraries may not make assumptions about where they are being consumed).

    The Solution?:

    Whatever is changing the serialNumber should also call trackSerialNumberUpdated(theModel)