From 77493927d9187c819956798e16026991ec92bffe Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 13 Jun 2025 11:59:57 -0700 Subject: [PATCH 1/3] Initial Deprecate Evented Mixin RFC --- text/1111-deprecate-evented-mixin.md | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 text/1111-deprecate-evented-mixin.md diff --git a/text/1111-deprecate-evented-mixin.md b/text/1111-deprecate-evented-mixin.md new file mode 100644 index 0000000000..b0cc7160e7 --- /dev/null +++ b/text/1111-deprecate-evented-mixin.md @@ -0,0 +1,76 @@ +--- +stage: accepted +start-date: 2025-06-13T00: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/1111 +project-link: +--- + + + +# Deprecating the `Evented` Mixin + +## Summary + +Deprecate the `Evented` Mixin in favor of just using the methods from `@ember/object/events`. + +## Motivation + +For a while now, Ember has not recommended the use of Mixins. In order to fully +deprecate Mixins, we need to deprecate all existing Mixins of which `Evented` is one. + +## Transition Path + +The `Evented` Mixin provides the following methods: `on`, `one`, `off`, `trigger`, and `has`. + +Of these all but `has` are just wrapping methods provided by `@ember/object/events` and +migration is straight-forward: + +* `obj.on(name, target, method?)` -> `addListener(obj, name, target, method)` +* `obj.one(name, target, method?)` -> `addListener(obj, name, target, method, true)` +* `obj.trigger(name, ...args)` -> `sendEvent(obj, name, args)` +* `obj.off(name, target, method?)` -> `removeListener(obj, name, target, method)` + +Unfortunately, `hasListeners` is not directly exposed by `@ember/object/events`. +We should consider exposing it as the others. In this case, the transition would be: + +* `obj.has(name)` -> `hasListeners(obj, name)` + +## How We Teach This + +In general, I think we should discourage the use of Evented and event handlers as +a core Ember feature and remove most references from the guids. + +## Drawbacks + +Many users probably rely on this functionality. However, it's almost certainly +something that we don't need to keep in Ember itself. + +## Alternatives + +* Convert `Evented` to a class decorator-style mixin. + +## Unresolved questions + +Should we inline the functionality of `Evented` into the classes that use it or are +we also deprecating the methods on those classes? From dad5e85812e652762e3e23e9321625cc12411c75 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 13 Jun 2025 12:15:25 -0700 Subject: [PATCH 2/3] Link to removal PR --- text/1111-deprecate-evented-mixin.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/text/1111-deprecate-evented-mixin.md b/text/1111-deprecate-evented-mixin.md index b0cc7160e7..d075de1d5d 100644 --- a/text/1111-deprecate-evented-mixin.md +++ b/text/1111-deprecate-evented-mixin.md @@ -56,6 +56,11 @@ We should consider exposing it as the others. In this case, the transition would * `obj.has(name)` -> `hasListeners(obj, name)` +## Exploration + +To validate this deprecation, I've tried removing the `Evented` Mixin from Ember.js in this PR: +https://github.com/emberjs/ember.js/pull/20917 + ## How We Teach This In general, I think we should discourage the use of Evented and event handlers as From 84e7ab3267e7c40e4aaaedcbc1960ed70d9d8802 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Tue, 12 Aug 2025 11:20:18 -0700 Subject: [PATCH 3/3] Update text to align with deprecation guide --- text/1111-deprecate-evented-mixin.md | 166 +++++++++++++++++++++++---- 1 file changed, 144 insertions(+), 22 deletions(-) diff --git a/text/1111-deprecate-evented-mixin.md b/text/1111-deprecate-evented-mixin.md index d075de1d5d..ee9a0734a2 100644 --- a/text/1111-deprecate-evented-mixin.md +++ b/text/1111-deprecate-evented-mixin.md @@ -28,54 +28,176 @@ prs: project-link: Leave as is --> -# Deprecating the `Evented` Mixin +# Deprecating `Ember.Evented` and `@ember/object/events` ## Summary -Deprecate the `Evented` Mixin in favor of just using the methods from `@ember/object/events`. +Deprecate the `Ember.Evented` mixin, the underlying `@ember/object/events` module (`addListener`, `removeListener`, `sendEvent`), and the `on()` function from `@ember/object/evented`. ## Motivation For a while now, Ember has not recommended the use of Mixins. In order to fully deprecate Mixins, we need to deprecate all existing Mixins of which `Evented` is one. +Further, the low-level event system in `@ember/object/events` predates modern +JavaScript features (classes, modules, native event targets, async / await) and +encourages an ad-hoc, implicit communication style that is difficult to statically +analyze and can obscure data flow. Removing it simplifies Ember's object model and +reduces surface area. Applications have many well-supported alternatives for +cross-object communication (services with explicit APIs, tracked state, resources, +native DOM events, AbortController-based signaling, promise-based libraries, etc.). + ## Transition Path -The `Evented` Mixin provides the following methods: `on`, `one`, `off`, `trigger`, and `has`. +The following are deprecated: + +* The `Ember.Evented` mixin +* The functions exported from `@ember/object/events` (`addListener`, `removeListener`, `sendEvent`) +* The `on()` function exported from `@ember/object/evented` +* Usage of the `Evented` methods (`on`, `one`, `off`, `trigger`, `has`) when mixed into framework classes (`Ember.Component`, `Ember.Route`, `Ember.Router`) + +Exception: The methods will continue to be supported (not deprecated) on the `RouterService`, since key parts of its functionality are difficult to reproduce without them. This RFC does not propose deprecating those usages. + +### Recommended Replacement Pattern + +Rather than mixing in a generic event emitter, we recommend refactoring affected code so that: + +1. A service (or other long‑lived owner-managed object) exposes explicit subscription methods (e.g. `onLoggedIn(cb)`), and +2. Internally uses a small event emitter implementation. We recommend the modern promise‑based [emittery](https://www.npmjs.com/package/emittery) library, though any equivalent (including a minimal custom implementation) is acceptable. + +This yields clearer public APIs, encapsulates implementation details, and makes teardown explicit by returning an unsubscribe function that can be registered with `registerDestructor`. + +### Example Migration + +Before (using `Evented`): + +```js +// app/services/session.js +import Service from '@ember/service'; +import Evented from '@ember/object/evented'; +import { tracked } from '@glimmer/tracking'; + +export default class SessionService extends Service.extend(Evented) { + @tracked user = null; + + login(userData) { + this.user = userData; + this.trigger('loggedIn', userData); + } + + logout() { + const oldUser = this.user; + this.user = null; + this.trigger('loggedOut', oldUser); + } +} +``` + +```js +// app/components/some-component.js +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { registerDestructor } from '@ember/destroyable'; + +export default class SomeComponent extends Component { + @service session; + + constructor(owner, args) { + super(owner, args); + this.session.on('loggedIn', this, 'handleLogin'); + registerDestructor(this, () => { + this.session.off('loggedIn', this, 'handleLogin'); + }); + } + + handleLogin(user) { + // ... update component state + } +} +``` + +After (using `emittery`): + +```js +// app/services/session.js +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import Emittery from 'emittery'; + +export default class SessionService extends Service { + @tracked user = null; + #emitter = new Emittery(); + + login(userData) { + this.user = userData; + this.#emitter.emit('loggedIn', userData); + } -Of these all but `has` are just wrapping methods provided by `@ember/object/events` and -migration is straight-forward: + logout() { + const oldUser = this.user; + this.user = null; + this.#emitter.emit('loggedOut', oldUser); + } -* `obj.on(name, target, method?)` -> `addListener(obj, name, target, method)` -* `obj.one(name, target, method?)` -> `addListener(obj, name, target, method, true)` -* `obj.trigger(name, ...args)` -> `sendEvent(obj, name, args)` -* `obj.off(name, target, method?)` -> `removeListener(obj, name, target, method)` + onLoggedIn(callback) { + return this.#emitter.on('loggedIn', callback); + } -Unfortunately, `hasListeners` is not directly exposed by `@ember/object/events`. -We should consider exposing it as the others. In this case, the transition would be: + onLoggedOut(callback) { + return this.#emitter.on('loggedOut', callback); + } +} +``` -* `obj.has(name)` -> `hasListeners(obj, name)` +```js +// app/components/some-component.js +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { registerDestructor } from '@ember/destroyable'; + +export default class SomeComponent extends Component { + @service session; + + constructor(owner, args) { + super(owner, args); + const unsubscribe = this.session.onLoggedIn((user) => this.handleLogin(user)); + registerDestructor(this, unsubscribe); + } + + handleLogin(user) { + // ... update component state + } +} +``` + +### Notes on Timing + +Libraries like `emittery` provide asynchronous (promise‑based) event emission by default. Code which previously depended on synchronous delivery ordering may need to be updated. If strict synchronous behavior is required, a synchronous emitter (custom or another library) can be substituted without changing the public API shape shown above. ## Exploration -To validate this deprecation, I've tried removing the `Evented` Mixin from Ember.js in this PR: -https://github.com/emberjs/ember.js/pull/20917 +To validate this deprecation, we explored removal of the `Evented` mixin from Ember.js core (see: https://github.com/emberjs/ember.js/pull/20917) and confirmed that its usage is largely isolated and can be shimmed or refactored at the application layer. ## How We Teach This -In general, I think we should discourage the use of Evented and event handlers as -a core Ember feature and remove most references from the guids. +* Update the deprecations guide (see corresponding PR in the deprecation app) with the migration example above. +* Remove most references to `Evented` from the Guides, replacing ad-hoc event usage examples with explicit service APIs. +* Emphasize explicit state and method calls, tracked state, resources, and native DOM events for orchestration. ## Drawbacks -Many users probably rely on this functionality. However, it's almost certainly -something that we don't need to keep in Ember itself. +* Applications relying heavily on synchronous event ordering may require careful refactors; asynchronous emitters change timing. +* Some addons may still expose `Evented`-based APIs and will need releases. +* Introduces a (small) external dependency when adopting an emitter library—though apps can implement a minimal sync emitter inline if desired. ## Alternatives -* Convert `Evented` to a class decorator-style mixin. +* Convert `Evented` to a decorator-style mixin (retains implicit pattern, less desirable). +* Keep `@ember/object/events` but deprecate only the mixin (adds partial complexity, limited long‑term value). +* Replace with a built-in minimal emitter utility instead of recommending third‑party (adds maintenance burden for Ember core). -## Unresolved questions +## Unresolved Questions -Should we inline the functionality of `Evented` into the classes that use it or are -we also deprecating the methods on those classes? +* Do we want to provide (or document) a canonical synchronous emitter alternative for cases where timing matters? +* Should we explicitly codemod support (e.g. generate service wrapper methods) or leave migration manual? +* Any additional framework internals still relying on these APIs that require staged removal?