Skip to content

Commit e8d0471

Browse files
authored
Merge pull request #1111 from wagenet/deprecate-evented-mixin
2 parents 15b65d1 + 84e7ab3 commit e8d0471

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
---
2+
stage: accepted
3+
start-date: 2025-06-13T00:00:00.000Z
4+
release-date:
5+
release-versions:
6+
teams: # delete teams that aren't relevant
7+
- cli
8+
- data
9+
- framework
10+
- learning
11+
- steering
12+
- typescript
13+
prs:
14+
accepted: https://github.com/emberjs/rfcs/pull/1111
15+
project-link:
16+
---
17+
18+
<!---
19+
Directions for above:
20+
21+
stage: Leave as is
22+
start-date: Fill in with today's date, 2032-12-01T00:00:00.000Z
23+
release-date: Leave as is
24+
release-versions: Leave as is
25+
teams: Include only the [team(s)](README.md#relevant-teams) for which this RFC applies
26+
prs:
27+
accepted: Fill this in with the URL for the Proposal RFC PR
28+
project-link: Leave as is
29+
-->
30+
31+
# Deprecating `Ember.Evented` and `@ember/object/events`
32+
33+
## Summary
34+
35+
Deprecate the `Ember.Evented` mixin, the underlying `@ember/object/events` module (`addListener`, `removeListener`, `sendEvent`), and the `on()` function from `@ember/object/evented`.
36+
37+
## Motivation
38+
39+
For a while now, Ember has not recommended the use of Mixins. In order to fully
40+
deprecate Mixins, we need to deprecate all existing Mixins of which `Evented` is one.
41+
42+
Further, the low-level event system in `@ember/object/events` predates modern
43+
JavaScript features (classes, modules, native event targets, async / await) and
44+
encourages an ad-hoc, implicit communication style that is difficult to statically
45+
analyze and can obscure data flow. Removing it simplifies Ember's object model and
46+
reduces surface area. Applications have many well-supported alternatives for
47+
cross-object communication (services with explicit APIs, tracked state, resources,
48+
native DOM events, AbortController-based signaling, promise-based libraries, etc.).
49+
50+
## Transition Path
51+
52+
The following are deprecated:
53+
54+
* The `Ember.Evented` mixin
55+
* The functions exported from `@ember/object/events` (`addListener`, `removeListener`, `sendEvent`)
56+
* The `on()` function exported from `@ember/object/evented`
57+
* Usage of the `Evented` methods (`on`, `one`, `off`, `trigger`, `has`) when mixed into framework classes (`Ember.Component`, `Ember.Route`, `Ember.Router`)
58+
59+
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.
60+
61+
### Recommended Replacement Pattern
62+
63+
Rather than mixing in a generic event emitter, we recommend refactoring affected code so that:
64+
65+
1. A service (or other long‑lived owner-managed object) exposes explicit subscription methods (e.g. `onLoggedIn(cb)`), and
66+
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.
67+
68+
This yields clearer public APIs, encapsulates implementation details, and makes teardown explicit by returning an unsubscribe function that can be registered with `registerDestructor`.
69+
70+
### Example Migration
71+
72+
Before (using `Evented`):
73+
74+
```js
75+
// app/services/session.js
76+
import Service from '@ember/service';
77+
import Evented from '@ember/object/evented';
78+
import { tracked } from '@glimmer/tracking';
79+
80+
export default class SessionService extends Service.extend(Evented) {
81+
@tracked user = null;
82+
83+
login(userData) {
84+
this.user = userData;
85+
this.trigger('loggedIn', userData);
86+
}
87+
88+
logout() {
89+
const oldUser = this.user;
90+
this.user = null;
91+
this.trigger('loggedOut', oldUser);
92+
}
93+
}
94+
```
95+
96+
```js
97+
// app/components/some-component.js
98+
import Component from '@glimmer/component';
99+
import { inject as service } from '@ember/service';
100+
import { registerDestructor } from '@ember/destroyable';
101+
102+
export default class SomeComponent extends Component {
103+
@service session;
104+
105+
constructor(owner, args) {
106+
super(owner, args);
107+
this.session.on('loggedIn', this, 'handleLogin');
108+
registerDestructor(this, () => {
109+
this.session.off('loggedIn', this, 'handleLogin');
110+
});
111+
}
112+
113+
handleLogin(user) {
114+
// ... update component state
115+
}
116+
}
117+
```
118+
119+
After (using `emittery`):
120+
121+
```js
122+
// app/services/session.js
123+
import Service from '@ember/service';
124+
import { tracked } from '@glimmer/tracking';
125+
import Emittery from 'emittery';
126+
127+
export default class SessionService extends Service {
128+
@tracked user = null;
129+
#emitter = new Emittery();
130+
131+
login(userData) {
132+
this.user = userData;
133+
this.#emitter.emit('loggedIn', userData);
134+
}
135+
136+
logout() {
137+
const oldUser = this.user;
138+
this.user = null;
139+
this.#emitter.emit('loggedOut', oldUser);
140+
}
141+
142+
onLoggedIn(callback) {
143+
return this.#emitter.on('loggedIn', callback);
144+
}
145+
146+
onLoggedOut(callback) {
147+
return this.#emitter.on('loggedOut', callback);
148+
}
149+
}
150+
```
151+
152+
```js
153+
// app/components/some-component.js
154+
import Component from '@glimmer/component';
155+
import { inject as service } from '@ember/service';
156+
import { registerDestructor } from '@ember/destroyable';
157+
158+
export default class SomeComponent extends Component {
159+
@service session;
160+
161+
constructor(owner, args) {
162+
super(owner, args);
163+
const unsubscribe = this.session.onLoggedIn((user) => this.handleLogin(user));
164+
registerDestructor(this, unsubscribe);
165+
}
166+
167+
handleLogin(user) {
168+
// ... update component state
169+
}
170+
}
171+
```
172+
173+
### Notes on Timing
174+
175+
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.
176+
177+
## Exploration
178+
179+
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.
180+
181+
## How We Teach This
182+
183+
* Update the deprecations guide (see corresponding PR in the deprecation app) with the migration example above.
184+
* Remove most references to `Evented` from the Guides, replacing ad-hoc event usage examples with explicit service APIs.
185+
* Emphasize explicit state and method calls, tracked state, resources, and native DOM events for orchestration.
186+
187+
## Drawbacks
188+
189+
* Applications relying heavily on synchronous event ordering may require careful refactors; asynchronous emitters change timing.
190+
* Some addons may still expose `Evented`-based APIs and will need releases.
191+
* Introduces a (small) external dependency when adopting an emitter library—though apps can implement a minimal sync emitter inline if desired.
192+
193+
## Alternatives
194+
195+
* Convert `Evented` to a decorator-style mixin (retains implicit pattern, less desirable).
196+
* Keep `@ember/object/events` but deprecate only the mixin (adds partial complexity, limited long‑term value).
197+
* Replace with a built-in minimal emitter utility instead of recommending third‑party (adds maintenance burden for Ember core).
198+
199+
## Unresolved Questions
200+
201+
* Do we want to provide (or document) a canonical synchronous emitter alternative for cases where timing matters?
202+
* Should we explicitly codemod support (e.g. generate service wrapper methods) or leave migration manual?
203+
* Any additional framework internals still relying on these APIs that require staged removal?

0 commit comments

Comments
 (0)