diff --git a/text/1234-deprecate-ember-object.md b/text/1234-deprecate-ember-object.md new file mode 100644 index 0000000000..465e958e6e --- /dev/null +++ b/text/1234-deprecate-ember-object.md @@ -0,0 +1,335 @@ +--- +stage: accepted +start-date: 2026-10-08T00:00:00.000Z +release-date: +release-versions: +teams: # delete teams that aren't relevant + - cli + - data + - framework + - learning + - steering + - typescript +prs: + accepted: https://github.com/emberjs/rfcs/pull/1234 +project-link: +--- + + + +# Deprecate EmberObject + +## Summary + +Deprecates `EmberObject` in a way that is initially opt-in, so people can more gradually prepare their codebase for the removal of `EmberObject`. + + +## Motivation + +`EmberObject` has had heavy use in the early days of Ember (pre-JavaScript having classes), and since classes shipped in 2015, the need for `EmberObject` has greatly diminished. + +Removing `EmberObject` is one of the last steps in coercing codebases to be plain modern JavaScript. + +## Transition Path + +Unlike previous deprecations, this is targeting Ember 9, and will have a feature flag that removes all behavior related to `EmberObject`. This does require a lot of internal implementation in `ember-source`, but is needed anyway for the removal of `EmberObject`, ultimately. + +This will be the first deprecation that users will be able to preview the removal of. + +Leading up to v8, the feature flag will be: + - for existing apps: "off" (someone who hasn't updated their `optional-features.json`): + - deprecation logged for not having this feature flag "on" + - EmberObject and all related APIs are still usable (unless the feature flag is "on") + - for the blueprint: "on" (the setting in `optional-features.json` is set to `true`: + - new apps cannot throw deprecations, so new apps get the benefits of this feature flag being "on" right away + +When the feature flag is "on": + - each API-to-be-removed will throw an error (until we ship the build-time feature-stripping for all the EmberObject and related code) + - ideally, setting the feature flag to "on" _removes_ all of the implementation for EmberObject, though this is not a blocker for the deprecation's behavior + - if we aren't able to implement removal in the initial release, we will implement the removal in a future minor release + +With the release of v8, and leading up to v9, the feature flag will be "on" by default: +- if users wish, the feature flag can be flipped back off, which brings back the EmberObject behavior along with the deprecation +- when no `optional-features.json` is present, or the `optional-features.json` does not contain the feature flag for this deprecation, the default value is assumed to be "on" + +At `ember-source` v9, `EmberObject` is removed fully along with the feature flag. + + +> [!NOTE] +> This includes `@computed`, as `@computed` is part of the "Ember Object Model" of reactivity. + + +Internally, implementation would likely be similar to how Mixins were initially deprecated -- copied to an "internal" file, and then the "public" version of `EmberObject` would override `init`, and provide the deprecations. + +On the internal copy of `EmberObject`, we deprecate all the methods (`get`, / `set` / etc), so that the deprecations flow through to other framework classes such as `Route`, `Controller`, `Service`, etc. + +## How We Teach This + +The guides have not taught `EmberObject` since Octane. The work is: + +- publish the deprecation guide below at [deprecations.emberjs.com](https://deprecations.emberjs.com) +- mark `EmberObject`, `@computed`, and the `@ember/object/computed` macros deprecated in the API docs, linking to the guide +- link the [Octane vs Classic cheat sheet](https://guides.emberjs.com/release/upgrading/current-edition/) from the deprecation message; it already has the before/afters + +### Deprecation Guide + +> [!NOTE] +> We are aware that this could be a very noisy deprecation, and are investigating ways to reduce that noise in a digestible way -- this _could_ mean only logging the deprecation once, for example - but would then mean people can only resolve one violation at a time after turning on the feature flag. + +```js +deprecate(message, false, { + id: 'deprecate-ember-object', + until: '9.0.0', + for: 'ember-source', + url: 'https://deprecations.emberjs.com/id/deprecate-ember-object', + since: { available: '7.x', enabled: '7.x' }, +}); +``` + +#### What is deprecated + +| | API | status | +| - | --- | ------ | +| 🌐 | `EmberObject` (default export of `@ember/object`) | **deprecated** | +| 🌐 | `this.get` / `this.set` / `setProperties` / `getProperties` / `incrementProperty` / `toggleProperty` / `notifyPropertyChange` on any class that extends `EmberObject`, including `Route`, `Controller`, `Service` | **deprecated** | +| 🌐 | `init`, `willDestroy`, `destroy`, `isDestroying`, `isDestroyed` as `EmberObject` methods | **deprecated** | +| 🌐 | `reopen` / `reopenClass` | **deprecated** | +| 🌐 | `@computed` and the `@ember/object/computed` macros | **deprecated** | + +Related deprecations with their own guides: + +- [`.extend()` / `.create()`](https://rfcs.emberjs.com/id/1117-deprecate-classic-classes) +- [Mixins](https://rfcs.emberjs.com/id/1116-deprecate-mixins) +- [observers](https://github.com/emberjs/rfcs/pull/1115) +- [`EmberArray` / `A()`](https://rfcs.emberjs.com/id/1114-deprecate-ember-array) +- [`ObjectProxy` / `ArrayProxy`](https://rfcs.emberjs.com/id/1112-deprecate-proxy) +- [`Evented`](https://rfcs.emberjs.com/id/1111-deprecate-evented-mixin) +- [`@ember/component`](https://rfcs.emberjs.com/id/1216-deprecate-ember-component) + +#### Migration + +These are only small examples. For a full guide, see the [Octane migration guides](https://guides.emberjs.com/v5.12.0/upgrading/current-edition/). + +
Your own class extends EmberObject + +```js +// before +import EmberObject from '@ember/object'; + +export default class Cart extends EmberObject { + items = []; + + init() { + super.init(...arguments); + this.total = 0; + } +} + +let cart = Cart.create({ currency: 'USD' }); +``` + +```js +// after +export default class Cart { + items = []; + total = 0; + + constructor({ currency }) { + this.currency = currency; + } +} + +let cart = new Cart({ currency: 'USD' }); +``` + +`create()` assigned every key of its argument onto the instance. A constructor receives the same object and assigns what it needs. + +
+ +
this.get / this.set + +```js +// before +this.set('count', this.get('count') + 1); +this.setProperties({ name, email }); +let { name, email } = this.getProperties('name', 'email'); +this.incrementProperty('count'); +this.toggleProperty('isOpen'); +this.get('user.address.city'); +``` + +```js +// after +this.count = this.count + 1; +Object.assign(this, { name, email }); +let { name, email } = this; +this.count++; +this.isOpen = !this.isOpen; +this.user?.address?.city; +``` + +Assignment only results in a rerender when the property is `@tracked`. [ember-tracked-properties-codemod](https://github.com/ember-codemods/ember-tracked-properties-codemod) adds `@tracked` to properties that `set` wrote to. The `ember/no-get` lint rule autofixes the reads. + +`notifyPropertyChange` has no replacement. With `@tracked`, the write is the notification. + +
+ +
@computed + +```js +// before +import { computed } from '@ember/object'; +import { alias, filterBy, sort } from '@ember/object/computed'; + +export default class Cart extends EmberObject { + @computed('items.@each.price') + get total() { + return this.items.reduce((sum, item) => sum + item.price, 0); + } + + @alias('user.name') owner; + @filterBy('items', 'isGift', true) gifts; + @sort('items', 'sortKeys') sorted; +} +``` + +```js +// after +import { cached } from '@glimmer/tracking'; + +export default class Cart { + @cached + get total() { + return this.items.reduce((sum, item) => sum + item.price, 0); + } + + get owner() { return this.user.name; } + get gifts() { return this.items.filter((item) => item.isGift); } + get sorted() { return this.items.toSorted(byKeys(this.sortKeys)); } +} +``` + +Dependent keys go away. A getter re-runs when any `@tracked` value it read changes. Use `@cached` only when the getter is expensive. + +
+ +
willDestroy / destroy() + +```js +// before +export default class Poller extends EmberObject { + init() { + super.init(...arguments); + this.timer = setInterval(this.tick, 1000); + } + + willDestroy() { + clearInterval(this.timer); + super.willDestroy(...arguments); + } +} + +poller.destroy(); +``` + +```js +// after +import { registerDestructor, destroy } from '@ember/destroyable'; + +export default class Poller { + constructor() { + this.timer = setInterval(this.tick, 1000); + registerDestructor(this, () => clearInterval(this.timer)); + } +} + +destroy(poller); +``` + +`isDestroying` and `isDestroyed` are also exported from `@ember/destroyable`. `Route`, `Controller`, and `Service` keep `willDestroy`. + +
+ +
Route, Controller, Service + +Only the `EmberObject` methods on these classes are deprecated: + +```js +// before +import Service from '@ember/service'; +import { computed } from '@ember/object'; + +export default class Session extends Service { + init() { + super.init(...arguments); + this.set('user', null); + } + + @computed('user') + get isLoggedIn() { + return Boolean(this.get('user')); + } +} +``` + +```js +// after +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +export default class Session extends Service { + @tracked user = null; + + get isLoggedIn() { + return Boolean(this.user); + } +} +``` + +
+ +
reopen / reopenClass + +```js +// before +Cart.reopen({ currency: 'USD' }); +Cart.reopenClass({ fromJSON(json) { /* ... */ } }); +``` + +```js +// after +export default class Cart { + currency = 'USD'; + + static fromJSON(json) { /* ... */ } +} +``` + +If the class is not yours, you may use the Presenter pattern for wrapping/enriching the source data. Addons that expected consumers to `reopen` their classes need to expose a configuration API instead. + +
+ +## Drawbacks + +keeping EmberObject is a drawback, because of the dozens of KB that come along with it. + +all codebases with old code probably have some usage of EmberObject remaining, so they need to migrate. + +## Alternatives + +- do nothing + +## Unresolved questions + +n/a