Array prototype extension deprecations within a model object

After upgrading my app to 5.12.0, I saw the deprecation warning about the prototype extensions. (Deprecate array prototype extensions | Ember.js - Deprecations)

I came across a scenario in my app that doesn’t appear to be covered by the examples: arrays embedded within a controller’s model.

Simplified example:

Template:

{{#each this.model.fruits as |fruit|}}
  {{fruit}}
{{/each}}

<button {{on 'click' this.addFruit}}>Add Fruit</button>

Controller:

  @action
  addFruit() {
    this.get('model.fruits’).pushObject(“apple");
  }

Route:

  Model is just an API post response put into an EmberObject, something like:
  { fruits: [ “apple”, “banana” ], favoriteFruit: “watermelon”, etc. } 

Once I change pushObject to push to address the deprecation, the template will no longer track new fruits.

I can work around this by calling notifyPropertyChange from addFruit, but that feels a bit heavy-handed.

Is there a best practice for this kind of scenario? Perhaps some way to add tracking to an array property within an EmberObject?

Should this be mentioned in the deprecation guide, either with the better solution (if there is one) or the notifyPropertyChange workaround (which took me a bit of digging to find)?

You’re right there’s not a lot of clarity around this sort of scenario but that’s partly due to the fact that there isn’t a one-size-fits-all solution. I would probably recommend moving away from notifyPropertyChange though. I’m not sure how long that’s going to be around.

The three general recommendations for handling autotracking with arrays or objects are:

1. Use the immutable/replace pattern:

This is the simplest approach but not necessarily the most performant. The nice part is that it’s “just Javascript” and easy to understand. Depending on your needs though it might be too simplistic.

  @tracked someArray = [];

  addFruit = (newFruit) => {
    this.someArray = [...this.someArray, newFruit];
  }

Autotracking is “shallow” so it will update whenever you update the tracked ref again. IIRC you can even do this:

    this.someArray.push(newFruit);
    this.someArray = this.someArray;

and it will work but I forget exactly.

Same deal with objects, you just need a new object ref.

2. Using tracked-built-ins

This is what the guides now recommend. This creates a “deep tracked” array or object (or other type) and all you really have to do is create your initial array with the special constructor:

import { TrackedArray } from 'tracked-built-ins';

class ShoppingList {
  items = new TrackedArray([]);

  addItem(item) {
    this.items.push(item);
  }
}
  1. BYO tracked object

You can define your own complex objects which have their own tracked properties. You can also use this strategy in conjunction with the two above. This gives you fine-grained control over your reactivity and your data formats. It may also be easier to use with TypeScript. This is great for scenarios when your data has a specific shape.

class FruitBasket {
  @tracked fruits = []; // could also use TrackedArray here
  @tracked favoriteFruit = '';

  @action addFruit(newFruit) {
    this.fruits = [...this.fruit, newFruit];
  }
}

Conclusion

Like I said there’s not necessarily a one-size-fits-all here because it depends on your needs and situation. In your case you could probably just do something like this:

Controller:

  @action
  addFruit(newFruit) {
    this.model = {
      ...this.model,
      ...{ fruits: [...this.model.fruits, newFruit] },
    }
  }

However that’s already getting a little complex so you may want to think about using strategy 2 or 3.

Thank you for the tips. Yes, I did see the guides for tracked built-ins, but the issue here is that it’s not a standalone object like in your ShoppingList example. The data lives within the overall model object. Using tracked built-ins (or option 1 for that matter) would require taking it out of that model and managing it separately from other model properties. That is not ideal, neither from a clarity nor a performance standpoint. notifyPropertyChange is not ideal either, but it at least lets me keep the model intact. The last option you mentioned is a bit clunky, but looks like it will do the same.