Skip to content

Commit 84e7ab3

Browse files
wagenetkategengler
authored andcommitted
Update text to align with deprecation guide
1 parent dad5e85 commit 84e7ab3

File tree

1 file changed

+144
-22
lines changed

1 file changed

+144
-22
lines changed

text/1111-deprecate-evented-mixin.md

Lines changed: 144 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,54 +28,176 @@ prs:
2828
project-link: Leave as is
2929
-->
3030

31-
# Deprecating the `Evented` Mixin
31+
# Deprecating `Ember.Evented` and `@ember/object/events`
3232

3333
## Summary
3434

35-
Deprecate the `Evented` Mixin in favor of just using the methods from `@ember/object/events`.
35+
Deprecate the `Ember.Evented` mixin, the underlying `@ember/object/events` module (`addListener`, `removeListener`, `sendEvent`), and the `on()` function from `@ember/object/evented`.
3636

3737
## Motivation
3838

3939
For a while now, Ember has not recommended the use of Mixins. In order to fully
4040
deprecate Mixins, we need to deprecate all existing Mixins of which `Evented` is one.
4141

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+
4250
## Transition Path
4351

44-
The `Evented` Mixin provides the following methods: `on`, `one`, `off`, `trigger`, and `has`.
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+
}
45135

46-
Of these all but `has` are just wrapping methods provided by `@ember/object/events` and
47-
migration is straight-forward:
136+
logout() {
137+
const oldUser = this.user;
138+
this.user = null;
139+
this.#emitter.emit('loggedOut', oldUser);
140+
}
48141

49-
* `obj.on(name, target, method?)` -> `addListener(obj, name, target, method)`
50-
* `obj.one(name, target, method?)` -> `addListener(obj, name, target, method, true)`
51-
* `obj.trigger(name, ...args)` -> `sendEvent(obj, name, args)`
52-
* `obj.off(name, target, method?)` -> `removeListener(obj, name, target, method)`
142+
onLoggedIn(callback) {
143+
return this.#emitter.on('loggedIn', callback);
144+
}
53145

54-
Unfortunately, `hasListeners` is not directly exposed by `@ember/object/events`.
55-
We should consider exposing it as the others. In this case, the transition would be:
146+
onLoggedOut(callback) {
147+
return this.#emitter.on('loggedOut', callback);
148+
}
149+
}
150+
```
56151

57-
* `obj.has(name)` -> `hasListeners(obj, name)`
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.
58176

59177
## Exploration
60178

61-
To validate this deprecation, I've tried removing the `Evented` Mixin from Ember.js in this PR:
62-
https://github.com/emberjs/ember.js/pull/20917
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.
63180

64181
## How We Teach This
65182

66-
In general, I think we should discourage the use of Evented and event handlers as
67-
a core Ember feature and remove most references from the guids.
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.
68186

69187
## Drawbacks
70188

71-
Many users probably rely on this functionality. However, it's almost certainly
72-
something that we don't need to keep in Ember itself.
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.
73192

74193
## Alternatives
75194

76-
* Convert `Evented` to a class decorator-style mixin.
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).
77198

78-
## Unresolved questions
199+
## Unresolved Questions
79200

80-
Should we inline the functionality of `Evented` into the classes that use it or are
81-
we also deprecating the methods on those classes?
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)