Deprecation Guide for Deprecate array prototype extensions
Ember historically extended the prototypes of native Javascript arrays to implement Ember.Enumerable, Ember.MutableEnumerable, Ember.MutableArray, Ember.Array. As of v5, the usages of array prototype extensions are deprecated.
To disable the extention of array prototypes, in config/environment.js, ensure that EXTEND_PROTOYPES is set to false on EmberENV:
EmberENV: { EXTEND_PROTOTYPES: false, // ... }, // ... Once it is set to false, audit your project for any breakage from the following methods no longer being available on native arrays. Exceptions will be thrown where they are in use:
For convenient functions like filterBy, compact, you can directly convert to use native array methods.
For mutation functions (like pushObject, replace) or observable properties (firstObject, lastObject), in order to keep the reactivity, you should take following steps:
- convert the array either to a new
@trackedproperty, or useTrackedArrayfromtracked-built-ins; - use array native methods;
- fully test to make sure the reactivity is maintained.
Convenient Functions
For convenient functions like filterBy, compact, you can directly convert to use native array methods. This includes following (a list from EmberArray methods):
any
Before:
someArray.any(callbackFn); After:
someArray.some(callbackFn); compact
Before:
someArray.compact(); After:
someArray = someArray.filter(val => val !== undefined && val !== null); filterBy
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.filterBy('food', 'beans'); // [{ food: 'beans', isFruit: false }] After:
let someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray = someArray.filter(el => el.food === 'beans'); // [{ food: 'beans', isFruit: false }] findBy
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.findBy('isFruit'); // { food: 'apple', isFruit: true } After:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.find(el => el.isFruit); // { food: 'apple', isFruit: true } getEach
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.getEach('food'); // ['apple', 'beans'] After:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.map(el => el.food); // ['apple', 'beans'] invoke
Before:
class Person { name; constructor(name) { this.name = name; } greet(prefix = 'Hello') { return `${prefix} ${this.name}`; } } [new Person('Tom'), new Person('Joe')].invoke('greet', 'Hi'); // ['Hi Tom', 'Hi Joe'] After:
class Person { name; constructor(name) { this.name = name; } greet(prefix = 'Hello') { return `${prefix} ${this.name}`; } } [new Person('Tom'), new Person('Joe')].map(person => person['greet']?.('Hi')); // ['Hi Tom', 'Hi Joe'] isAny
Before
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.isAny('isFruit'); // true After:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.some(el => el.isFruit); // true isEvery
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.isEvery('isFruit'); // false After:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.every(el => el.isFruit); // false mapBy
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.mapBy('food'); // ['apple', 'beans'] After:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.map(el => el.food); // ['apple', 'beans'] objectAt
Before
const someArray = [1, 2, 3, undefined]; someArray.objectAt(1); // 2 After:
const someArray = [1, 2, 3, undefined]; someArray[1] // 2 objectsAt
Before:
const someArray = [1, 2, 3, undefined]; someArray.objectsAt([1, 2]); // [2, 3] After:
const someArray = [1, 2, 3, undefined]; [1, 2].map(index => someArray[index]); //[2, 3] reject
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.reject(el => el.isFruit); // [{ food: 'beans', isFruit: false }] After:
let someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray = someArray.filter(el => !el.isFruit); // [{ food: 'beans', isFruit: false }] rejectBy
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.rejectBy('isFruit'); // [{ food: 'beans', isFruit: false }] After:
let someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray = someArray.filter(el => !el.isFruit); // [{ food: 'beans', isFruit: false }] sortBy
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.sortBy('food', 'isFruit'); // [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }] After:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; [...someArray].sort((a, b) => { return a.food?.localeCompare(b.food) ? a.food?.localeCompare(b.food) : a.isFruit - b.isFruit; }); // [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }] toArray
Before:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; someArray.toArray(); // [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }] After:
const someArray = [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }]; [...someArray] // [{ food: 'apple', isFruit: true }, { food: 'beans', isFruit: false }] uniq
Before:
const someArray = [1, 2, 3, undefined, 3]; someArray.uniq(); // [1, 2, 3, undefined] After:
const someArray = [1, 2, 3, undefined, 3]; [...new Set(someArray)] // [1, 2, 3, undefined] uniqBy
Before:
const someArray = [{ food: 'apple' }, { food: 'beans' }, { food: 'apple' }]; someArray.uniqBy('food'); // [{ food: 'apple' }, { food: 'beans' }] After:
const someArray = [{ food: 'apple' }, { food: 'beans' }, { food: 'apple' }]; someArray.reduce( (unique, item) => { if (!unique.find(i => item.food === i.food)) { unique.push(item); } return unique; }, [] ); // [{ food: 'apple' }, { food: 'beans' }] You may also instead rely on methods from another library like lodash. Keep in mind that different libraries will behave in slightly different ways, so make sure any critical transformations are thoroughly tested.
Some special cases
without
Before
const someArray = ['a', 'b', 'c']; someArray.without('a'); // ['b', 'c'] After
let someArray = ['a', 'b', 'c']; someArray = someArray.filter(el => el !== 'a'); // ['b', 'c'] Please make sure without reactivity is fully tested.
setEach
setEach method internally implements set which responds to reactivity. You can either also use set or convert to @tracked properties.
Before
const items = [{ name: 'Joe' }, { name: 'Matt' }]; items.setEach('zipCode', '10011'); // items = [{ name: 'Joe', zipCode: '10011' }, { name: 'Matt', zipCode: '10011' }] After
// use `set` import { set } from '@ember/object'; const items = [{ name: 'Joe' }, { name: 'Matt' }]; items.forEach(item => { set(item, 'zipCode', '10011'); }); // items = [{ name: 'Joe', zipCode: '10011' }, { name: 'Matt', zipCode: '10011' }] or
// use `@tracked` import { tracked } from '@glimmer/tracking'; class Person { name; @tracked zipCode; constructor({ name, zipCode }) { this.name = name; this.zipCode = zipCode; } } const items = new TrackedArray([ new Person({ name: 'Joe' }), new Person({ name: 'Matt' }), ]); items.forEach(item => { item.zipCode = '10011'; }); // items = [{ name: 'Joe', zipCode: '10011' }, { name: 'Matt', zipCode: '10011' }] Observable Properties
firstObject, lastObject are observable properties. Changing directly from firstObject to at(0) or [0] might cause issues that the properties are no longer reactive.
Used in template
If the firstObject and lastObject are used in a template, you can convert to use get helper safely as get helper handles the reactivity already.
Before
<Foo @bar={{@list.firstObject.name}} /> After
<Foo @bar={{get @list '0.name'}} /> You can also leverage fixers provided by ember-template-lint/no-array-prototype-extensions.
Used in js
If the firstObject and lastObject are used in js files and you used them in an observable way, you will need to convert the accessors to @tracked array or TrackedArray.
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; // lastObj will change when `someAction` is executed get lastObj() { return this.abc.lastObject; } @action someAction(value) { this.abc.pushObject(value); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); get lastObj() { return this.abc.at(-1); } @action someAction(value) { this.abc.push(value); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = []; get lastObj() { return this.abc.at(-1); } @action someAction(value) { this.abc = [...this.abc, value]; } } Mutation methods
Mutation methods are observable-based, which means you should always convert the accessors to @tracked or TrackedArray in order to maintain the reactivity. This includes following (a list from MutableArray methods):
addObject
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action addObject(value) { this.abc.addObject(value); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action addObject(value) { if (!this.abc.includes(value)) { this.abc.push(value); } } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action addObject(value) { if (!this.abc.includes(value)) { this.abc = [...this.abc, value]; } } } addObjects
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action addObjects(value) { this.abc.addObjects(value); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); _addObject(value) { if (!this.abc.includes(value)) { this.abc.push(value); } } @action addObjects(values) { values.forEach(v => this._addObject(v)) } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; _addObject(value) { if (!this.abc.includes(value)) { this.abc = [...this.abc, value]; } } @action addObjects(values) { values.forEach(v => this._addObject(v)) } } clear
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action clear(value) { this.abc.clear(); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action clear(value) { this.abc.splice(0, this.abc.length); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action clear() { this.abc = []; } } insertAt
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action insertAt(idx, value) { this.abc.insertAt(idx, value); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action insertAt(idx, value) { this.abc.splice(idx, 0, value); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action insertAt(idx, value) { this.abc = [...this.abc.slice(0, idx), value, this.abc.slice(this.abc.length - idx)] } } popObject
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action popObject() { this.abc.popObject(); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action popObject() { this.abc.pop(); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action popObject() { this.abc.pop(); this.abc = [...this.abc]; } } pushObject
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action pushObject(value) { this.abc.pushObject(value); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action pushObject(value) { this.abc.push(value); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action pushObject(value) { this.abc = [...this.abc, value]; } } pushObjects
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action pushObjects(values) { this.abc.pushObjects(values); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action pushObjects(values) { this.abc.push(...values); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action pushObjects(values) { this.abc = [...this.abc, ...values]; } } removeAt
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action removeAt(start, len) { this.abc.removeAt(start, len); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action removeAt(start, len) { this.abc.splice(start, len); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action removeAt(start, len) { this.abc.splice(start, len); this.abc = this.abc; } } removeObject
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action removeObject(value) { this.abc.removeObject(value); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action removeObject(value) { const index = this.abc.indexOf(value); if (index !== -1) { this.abc.splice(index, 1); } } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action removeObject(value) { let loc = this.abc.length || 0; while (--loc >= 0) { let curValue = this.abc.at(loc); if (curValue === value) { this.abc.splice(loc, 1); } } this.abc = [...this.abc]; } } removeObjects
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action removeObjects(values) { this.abc.removeObjects(values); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); _removeObject(value) { let loc = this.abc.length || 0; while (--loc >= 0) { let curValue = this.abc.at(loc); if (curValue === value) { this.abc.splice(loc, 1); } } } @action removeObjects(values) { values.forEach(v => { this._removeObject(v); }); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; _removeObject(value) { let loc = this.abc.length || 0; while (--loc >= 0) { let curValue = this.abc.at(loc); if (curValue === value) { this.abc.splice(loc, 1); } } this.abc = [...this.abc]; } @action removeObjects(values) { values.forEach(v => { this._removeObject(v); }) } } replace
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action replace(idx, len, values) { this.abc.replace(idx, len, values); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action replace(idx, len, values) { this.abc.splice(idx, len, ...values); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action replace(idx, len, values) { this.abc.splice(idx, len, ...values); this.abc = [...this.abc]; } } reverseObjects
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action reverseObjects() { this.abc.reverseObjects(); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action reverseObjects() { this.abc.reverse(); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action reverseObjects() { this.abc = [...this.abc.reverse()]; } } setObjects
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action setObjects(values) { this.abc.setObjects(values); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action setObjects(values) { this.abc.splice(0, this.abc.length, ...values); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action setObjects(values) { this.abc = [...values]; } } shiftObject
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action shiftObject() { this.abc.shiftObject(); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action shiftObject() { this.abc.shift(); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action shiftObject() { this.abc.shift(); this.abc = [...this.abc] } } unshiftObject
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action unshiftObject(obj) { this.abc.unshiftObject(obj); } } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action unshiftObject(obj) { this.abc.unshift(obj); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action unshiftObject(obj) { this.abc.unshift(obj); this.abc = [...this.abc]; } } unshiftObjects
Before
import Component from '@glimmer/component'; export default class SampleComponent extends Component { abc = ['x', 'y', 'z', 'x']; @action unshiftObjects(objs) { this.abc.unshiftObjects(objs); } After
// TrackedArray import Component from '@glimmer/component'; import { action } from '@ember/object'; import { TrackedArray } from 'tracked-built-ins'; export default class SampleComponent extends Component { abc = new TrackedArray(['x', 'y', 'z', 'x']); @action unshiftObjects(objs) { this.abc.unshift(...objs); } } or
// @tracked import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class SampleComponent extends Component { @tracked abc = ['x', 'y', 'z', 'x']; @action unshiftObjects(objs) { this.abc.unshift(...objs) this.abc = [...this.abc]; } } It's always recommended to reference the existing implementation of the method you are trying to convert. This can make sure functionalities are kept as it was. Implementation details can be found in MutableArray, for example removeObject.