Start Date Release Date Release Versions PR link Tracking Link Stage Teams
6/13/2025
Accepted
  • CLI
  • Data
  • Framework
  • Learning
  • Steering
  • TypeScript

Deprecating Ember.Evented and @ember/object/events

Summary

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 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.

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 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):

// 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);
  }
}
// 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):

// 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);
  }

  logout() {
    const oldUser = this.user;
    this.user = null;
    this.#emitter.emit('loggedOut', oldUser);
  }

  onLoggedIn(callback) {
    return this.#emitter.on('loggedIn', callback);
  }

  onLoggedOut(callback) {
    return this.#emitter.on('loggedOut', callback);
  }
}
// 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, 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

  • 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

  • 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 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

  • 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?