Skip to content

Commit

Permalink
Merge pull request #2054 from ember-learn/proto-extension-cleanup
Browse files Browse the repository at this point in the history
Removing prototype extension usage and @ember/array in favor of tracked built ins
  • Loading branch information
ef4 authored Sep 20, 2024
2 parents fd850f7 + 123765b commit e2d53ec
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 137 deletions.
106 changes: 35 additions & 71 deletions guides/release/configuring-ember/disabling-prototype-extensions.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
By default, Ember.js will extend the prototypes of native JavaScript
arrays to implement the `Ember.Enumerable`, `Ember.MutableEnumerable`,
`Ember.MutableArray` and `Ember.Array` interfaces. This polyfills
ECMAScript 5 array methods in browsers that do not implement them, adds
convenience methods and properties to built-in arrays, and makes array
mutations observable.

This is the extent to which Ember.js enhances native prototypes. We have
carefully weighed the trade-offs involved with changing these prototypes,
and recommend that most Ember.js developers use them. These extensions
significantly reduce the amount of boilerplate code that must be typed.

However, we understand that there are cases where your Ember.js
application may be embedded in an environment beyond your control. The
most common scenarios are when authoring third-party JavaScript that is
embedded directly in other pages, or when transitioning an application
piecemeal to a more modern Ember.js architecture.

In those cases, where you can't or don't want to modify native
prototypes, Ember.js allows you to completely disable the extensions
described above.

To do so, simply set the `EmberENV.EXTEND_PROTOTYPES` flag to `false`:
<div class="cta">
<div class="cta-note">
<div class="cta-note-body">
<div class="cta-note-heading">Zoey says...</div>
<div class="cta-note-message">
<em>Not</em> disabling prototype extensions is deprecated at Ember 5.10 and will be removed at Ember 6.0. See <a href="https://deprecations.emberjs.com/id/deprecate-array-prototype-extensions">the deprecation guide</a> for more detail.
</div>
</div>
<img src="/images/mascots/zoey.png" role="presentation" alt="">
</div>
</div>

Historically, Ember.js extended the prototypes of native JavaScript arrays to
implement the `Ember.Enumerable`, `Ember.MutableEnumerable`,
`Ember.MutableArray` and `Ember.Array` interfaces. This is the default behavior
up until 6.0, at which point it will no longer be supported.

To prepare for 6.0, you can disable prototype extensions immediately. To do so,
set the `EmberENV.EXTEND_PROTOTYPES` flag to `false`:

```javascript {data-filename=config/environment.js}
ENV = {
Expand All @@ -30,52 +26,36 @@ ENV = {
}
```

You can configure which classes to include prototype extensions
for in your application's configuration like so:
## Life Without Array Prototype Extension

```javascript {data-filename=config/environment.js}
ENV = {
EmberENV: {
EXTEND_PROTOTYPES: {
Array: false
}
}
}
```
There are two major differences to how arrays will behave after you disable array prototype extensions.

## Life Without Prototype Extension
### No more non-standard methods

In order for your application to behave correctly, you will need to
manually extend or create the objects that the native objects were
creating before.
Arrays no longer have the non-standard methods listed in the [deprecation guide](https://deprecations.emberjs.com/id/deprecate-array-prototype-extensions) like `pushObject`, etc. Follow the deprecation guide to replace each usage with a standard JavaScript alternative.

### Arrays
### Tracking of Changes in Arrays

Native arrays will no longer implement the functionality needed to
observe them. If you disable prototype extension and attempt to use
If you disable prototype extensions and attempt to use
native arrays with things like a template's `{{#each}}` helper, Ember.js
will have no way to detect changes to the array and the template will
not update as the underlying array changes.

You can manually coerce a native array into an array that implements the
required interfaces using the convenience method `Ember.A`:
You can restore automatic tracking of changes by replacing your native array with a `TrackedArray` from the 'tracked-built-ins' library.

```javascript
import { A } from '@ember/array';

let islands = ['Oahu', 'Kauai'];
islands.pushObject('Maui');
// => TypeError: Object Oahu,Kauai has no method `pushObject`

// Convert `islands` to an array that implements the
// Ember enumerable and array interfaces
A(islands);
import { TrackedArray } from '@glimmer/tracking';

islands.pushObject('Maui');
// => ['Oahu', 'Kauai', 'Maui'];
class Ocean {
islands = new TrackedArray(['Oahu', 'Kauai']);

addIsland(newIsland) {
this.islands.push(newIsland);
}
}
```

You can also use an "immutable update" style with tracked properties:
Alternatively, you can refactor your code to use an "immutable update" style with tracked properties:

```javascript
import { tracked } from '@glimmer/tracking';
Expand All @@ -87,22 +67,6 @@ class Ocean {
this.islands = this.islands.concat(newIsland);
}
}

const ocean = new Ocean();
ocean.addIsland('Maui');
ocean.islands; // => ['Oahu', 'Kauai', 'Maui'];
```

Alternatively, you can use the community library `tracked-built-ins`
to get a natively tracked version of `Array`, and use native `Array`
methods with auto-tracking reactivity:

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

let islands = new TrackedArray(['Oahu', 'Kauai']);
islands.push('Maui');
// => ['Oahu', 'Kauai', 'Maui'];
```

<!-- eof - needed for pages that end in a code block -->
47 changes: 21 additions & 26 deletions guides/release/in-depth-topics/autotracking-in-depth.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,51 +276,46 @@ Generally, you should try to create classes with their tracked properties
enumerated and decorated with `@tracked`, instead of relying on dynamically
created POJOs. In some cases however, if your usage of properties on POJOs is
too dynamic, you may not be able to enumerate every single property that could
be tracked. There could be a prohibitive number of possible properties, or there
could be no way to know them in advance. In this case, it's recommended that you
_reset_ the value wherever it is updated:
be tracked. In this case, you can use `TrackedObject` from `tracked-built-ins`:

```js
class SimpleCache {
@tracked _cache = {};
import { TrackedObject } from 'tracked-built-ins';

set(key, value) {
this._cache[key] = value;
let obj = new TrackedObject({
a: 1,
b: 2,
})

// trigger an update
this._cache = this._cache;
}

get(key) {
return this._cache[key];
}
}
// This change is tracked
obj.c = 3;
```

Triggering an update like this will cause any getters that used the `cache` to
recalculate. Note that we can use the `get` method to access the cache, and it
will still push the `_cache` tracked property.
All property reading and writing on this object is automatically tracked.
`TrackedObject` is "shallowly" tracked. `obj.c = 4` would be tracked, but
`obj.c.somethingDeeper = 5` would not be tracked unless you've also made sure
that the contents of `obj.c` is itself another `TrackedObject`.


#### Arrays

Arrays are another example of a type of object where you can't enumerate every
possible value - after all, there are an infinite number of integers (though you
_may_ run out of bits in your computer at some point!). Instead, you can
continue to use `EmberArray`, which will continue to work with tracking and will
cause any dependencies that use it to invalidate correctly.
When you want to track the contents of an Array, you can use `TrackedArray` from
`tracked-built-ins`:

```js
import { A } from '@ember/array';
import { TrackedArray } from 'tracked-built-ins';

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

addItem(item) {
this.items.pushObject(item);
this.items.push(item);
}
}
```

`TrackedArray` supports all the normal native `Array` methods, ensuring that
their reads and writes are tracked.

## Caching of tracked properties

In contrast to computed properties from pre-Octane, tracked properties are not
Expand Down
2 changes: 1 addition & 1 deletion guides/release/routing/query-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default class ArticlesController extends Controller {
let articles = this.model;

if (category) {
return articles.filterBy('category', category);
return articles.filter((article) => article.category === category);
} else {
return articles;
}
Expand Down
12 changes: 6 additions & 6 deletions guides/release/services/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,22 @@ Like any Ember object, a service is initialized and can have properties and meth
Below, the shopping cart service manages an items array that represents the items currently in the shopping cart.

```javascript {data-filename=app/services/shopping-cart.js}
import { A } from '@ember/array';
import { TrackedArray } from 'tracked-built-ins';
import Service from '@ember/service';

export default class ShoppingCartService extends Service {
items = A([]);
items = new TrackedArray([]);

add(item) {
this.items.pushObject(item);
this.items.push(item);
}

remove(item) {
this.items.removeObject(item);
remove(item)
this.items.splice(this.items.indexOf(item), 1);
}

empty() {
this.items.clear();
this.items.splice(0, this.items.length);
}
}
```
Expand Down
60 changes: 27 additions & 33 deletions guides/release/upgrading/current-edition/tracked-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,8 @@ Tracked properties have subtler benefits as well:
to anything external to your class.

Most computed properties should be fairly straightforward to convert to tracked
properties. It's important to note that in these new components, arguments are
automatically tracked, but in classic components they are _not_. This is because
arguments are put on the `args` hash, which is tracked
property. Since they are assigned to arbitrary properties on classic components,
they can't be instrumented ahead of time, so you must decorate them manually.
properties. It's important to note that on `@glimmer/component`, arguments are
automatically tracked, but in classic `@ember/component` they are not.

#### Plain Old JavaScript Objects (POJOs)

Expand Down Expand Up @@ -130,53 +127,50 @@ class Person {
}
```

In some cases, if your usage of properties on POJOs is too dynamic, you may not
be able to enumerate every single property that could be tracked. There could be
a prohibitive number of possible properties, or there could be no way to know
them in advance. In this case, it's recommended that you _reset_ the value
wherever it is updated:
Generally, you should try to create classes with their tracked properties
enumerated and decorated with `@tracked`, instead of relying on dynamically
created POJOs. In some cases however, if your usage of properties on POJOs is
too dynamic, you may not be able to enumerate every single property that could
be tracked. In this case, you can use `TrackedObject` from `tracked-built-ins`:

```js
class SimpleCache {
@tracked _cache = {};
import { TrackedObject } from 'tracked-built-ins';

set(key, value) {
this._cache[key] = value;
let obj = new TrackedObject({
a: 1,
b: 2,
})

// trigger an update
this._cache = this._cache;
}

get(key) {
return this._cache[key];
}
}
// This change is tracked
obj.c = 3;
```

Triggering an update like this will cause any getters that used the `_cache` to
recalculate. Note that we can use the `get` method to access the cache, and it
will still push the `_cache` tracked property.
All property reading and writing on this object is automatically tracked.
`TrackedObject` is "shallowly" tracked. `obj.c = 4` would be tracked, but
`obj.c.somethingDeeper = 5` would not be tracked unless you've also made sure
that the contents of `obj.c` is itself another `TrackedObject`.


#### Arrays

Arrays are another example of a type of object where you can't enumerate every
possible value - after all, there are an infinite number of integers (though you
_may_ run out of bits in your computer at some point!). Instead, you can
continue to use `EmberArray`, which will continue to work with tracking and will
cause any dependencies that use it to invalidate correctly.
When you want to track the contents of an Array, you can use `TrackedArray` from
`tracked-built-ins`:

```js
import { A } from '@ember/array';
import { TrackedArray } from 'tracked-built-ins';

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

addItem(item) {
this.items.pushObject(item);
this.items.push(item);
}
}
```

`TrackedArray` supports all the normal native `Array` methods, ensuring that
their reads and writes are tracked.

### Backwards Compatibility

Tracked properties are fully backwards compatible with computed properties and
Expand Down

0 comments on commit e2d53ec

Please sign in to comment.