From 0bec353c75e006a713f120bbb4d4c88702fd4a47 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:37:12 -0400 Subject: [PATCH] Update #1116's missing deprecation guides --- text/1116-deprecate-mixins.md | 377 +++++++++++++++++++++++++++++++++- 1 file changed, 375 insertions(+), 2 deletions(-) diff --git a/text/1116-deprecate-mixins.md b/text/1116-deprecate-mixins.md index 572d4c96af..c23997d077 100644 --- a/text/1116-deprecate-mixins.md +++ b/text/1116-deprecate-mixins.md @@ -39,13 +39,386 @@ For users that still want mixin-like functionality, we should recommend class decorators. If this feels less ergonimic that we would desire, we can consider providing an addon that adds some sugar around this. +### Deprecation guides + +The deprecation guides for each of the above are written out below, and are being +published to https://deprecations.emberjs.com/. Each guide targets `until: 8.0.0`. + +#### `@ember/object/mixin` (since 7.4.0) + +`Mixin` from `@ember/object/mixin` is deprecated. Mixins are part of the legacy `EmberObject` class system and do not work with native class syntax. + +If your code still uses classic class syntax, convert it to native classes first, for example with the [ember-native-class-codemod](https://github.com/ember-codemods/ember-native-class-codemod). Then remove the mixins using one of the patterns below. + +##### Before: sharing behavior with a mixin + +```javascript +// app/mixins/editable.js +import Mixin from '@ember/object/mixin'; + +export default Mixin.create({ + isEditing: false, + + edit() { + this.set('isEditing', true); + }, +}); +``` + +```javascript +// app/models/comment.js +import EmberObject from '@ember/object'; +import EditableMixin from '../mixins/editable'; + +export default class Comment extends EmberObject.extend(EditableMixin) { + post = null; +} +``` + +##### After: sharing behavior with a subclass factory + +A function that takes a base class and returns a subclass provides the same composition using only standard JavaScript: + +```javascript +// app/utils/editable.js +import { tracked } from '@glimmer/tracking'; + +export function editable(Base) { + return class extends Base { + @tracked isEditing = false; + + edit() { + this.isEditing = true; + } + }; +} +``` + +```javascript +// app/models/comment.js +import EmberObject from '@ember/object'; +import { editable } from '../utils/editable'; + +export default class Comment extends editable(EmberObject) { + post = null; +} +``` + +Because the factory returns a class expression, it also works as a class decorator (`@editable`) if your build is configured for decorator syntax on classes. + +##### Other replacements + +Depending on what a mixin was doing, a different pattern may fit better: + +- Shared state or behavior used across the app belongs in a service or service-like abstraction, injected where needed. +- Stateless helpers can be plain functions exported from a module (these are also easier to unit test). +- Behavior only used by one class hierarchy can move into a common base class (though composition is **strongly** recommended instead of inheritance). + +#### `@ember/object/promise-proxy-mixin` (since 7.4.0) + +`PromiseProxyMixin` from `@ember/object/promise-proxy-mixin` is deprecated. Use `async/await` with tracked state instead. + +Before: + +```gjs +import ObjectProxy from '@ember/object/proxy'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; + +const PromiseObject = ObjectProxy.extend(PromiseProxyMixin); + +const proxy = PromiseObject.create({ + promise: fetchSettings(), +}); + + +``` + +##### After: tracked properties and `async/await` + +Manage the state of the asynchronous operation in the class that needs it: + +```gjs +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +export default class Settings extends Component { + @tracked isLoading = true; + @tracked error = null; + @tracked content = null; + + constructor() { + super(...arguments); + this.loadData(); + } + + async loadData() { + try { + this.content = await fetchSettings(); + } catch (error) { + this.error = error; + } finally { + this.isLoading = false; + } + } + + +} +``` + +##### After: an ember-concurrency task + +For user-initiated or cancellable work, [ember-concurrency](https://ember-concurrency.com/docs/introduction) tasks expose the same lifecycle state that `PromiseProxyMixin` provided, along with cancellation and concurrency control: + +```gjs +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; + +export default class Settings extends Component { + load = () => void this.loadData.perform(); + + loadData = task(async () => { + return await fetchSettings(); + }); + + +} +``` + +##### Migration strategy + +Pick a replacement based on the use case: + +1. For user-initiated async tasks (button clicks, form submissions), use ember-concurrency. +2. For data loading, consider [WarpDrive](https://warp-drive.io) (formerly Ember Data), whose `getRequestState` tracks request state for you (see also: [`getPromiseState`](https://reactive.nullvoxpopuli.com/functions/get-promise-state.getPromiseState.html) if not using WarpDrive). + +#### `@ember/object/comparable` (since 7.2.0) + +The `Comparable` mixin is deprecated. + +Apps and addons should stop importing or extending `Comparable`. To provide custom comparison behavior, define a function-valued `compare(other)` method directly on the object or class instead. + +Before: + +```js +import EmberObject from '@ember/object'; +import Comparable from '@ember/-internals/runtime/lib/mixins/comparable'; + +const Rectangle = EmberObject.extend(Comparable, { + compare(other) { + // custom comparison logic + }, +}); +``` + +After: + +```js +class Rectangle { + // Returns -1, 0, 1 + compare(other) { + // custom comparison logic + }, +} +``` + +Defining `compare(other)` directly is sufficient. + +However, if you need to `sort` a list of your objects, you will want to define a separate [`comparator function`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description). + +#### `@ember/enumerable` and `@ember/enumerable/mutable` (since 7.4.0) + +`Enumerable` from `@ember/enumerable` and `MutableEnumerable` from `@ember/enumerable/mutable` are deprecated. + +These mixins have been empty for a long time. The mixins were kept only so that existing `.detect()` checks kept working. They are now deprecated along with the rest of the mixin system. + +##### Replacing `.detect()` checks + +`Enumerable.detect(value)` answered "can I treat this value as a list?". Check for the capability you need instead. + +Before: + +```javascript +import Enumerable from '@ember/enumerable'; + +function printAll(maybeList) { + if (Enumerable.detect(maybeList)) { + maybeList.forEach((item) => console.log(item)); + } +} +``` + +After, for arrays: + +```javascript +function printAll(maybeList) { + if (Array.isArray(maybeList)) { + maybeList.forEach((item) => console.log(item)); + } +} +``` + +Or, to accept any iterable (`Map`, `Set`, generators, and so on): + +```javascript +function printAll(maybeList) { + if (typeof maybeList?.[Symbol.iterator] === 'function') { + for (let item of maybeList) { + console.log(item); + } + } +} +``` + +##### Replacing custom enumerable classes + +If you built a custom collection class with these mixins (usually through `EmberArray` or `MutableArray`), switch to a native array, or to `trackedArray` from [@ember/reactive/collections](https://api.emberjs.com/ember/release/modules/@ember%2Freactive%2Fcollections/) when the collection drives UI updates. + +For collection classes with their own API, implement the [iterable protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) so consumers can use `for...of`, spread, and `Array.from`: + +```javascript +class Queue { + #items = []; + + add(item) { + this.#items.push(item); + } + + *[Symbol.iterator]() { + yield* this.#items; + } +} + +let queue = new Queue(); +queue.add('a'); +queue.add('b'); + +[...queue]; // ['a', 'b'] +``` + +The enumerable methods themselves (`firstObject`, `mapBy`, `pushObject`, and friends) live on `EmberArray` and `MutableArray`, which are deprecated as well. Replace them with native equivalents such as `arr[0]`, `map`, and `push`, using `trackedArray` where mutation needs to be tracked. + +#### `@ember/object/observable` — `this.get` and `this.set` (since 7.4.0) + +The `get` and `set` methods from `@ember/object/observable` are deprecated. This also applies to all built-in `Ember.Object` descendants. +To migrate, use native JavaScript getters and setters instead. + +##### Replacing `.get()` + +Instead of using `.get()`, use standard property access. + +**Before** + +```javascript +import EmberObject from '@ember/object'; + +class Person extends EmberObject { + name = 'John Doe'; + details = { + age: 30 + }; +} + +const person = new Person(); + +const name = person.get('name'); +const age = person.get('details.age'); +``` + +**After** + +```javascript +class Person { + name = 'John Doe'; + details = { + age: 30 + }; +} + +const person = new Person(); + +const name = person.name; +const age = person.details.age; +``` + +For nested properties that might be null or undefined, use the optional chaining operator (`?.`): + +```javascript +const street = person.address?.street; +``` + +##### Replacing `.set()` + +Instead of using `.set()`, use standard property assignment. + +**Before** + +```javascript +import EmberObject from '@ember/object'; + +class Person extends EmberObject { + name = 'John Doe'; +} + +const person = new Person(); + +person.set('name', 'Jane Doe'); +``` + +**After** + +```javascript +import { tracked } from '@glimmer/tracking'; + +class Person { + @tracked name = 'John Doe'; +} + +const person = new Person(); + +person.name = 'Jane Doe'; +``` + ## Exploration To validate this deprecation, I've tried removing all Mixins in this PR: https://github.com/emberjs/ember.js/pull/20923 ## How We Teach This -We should remove all references from the guides and provide a [deprecation guide](https://github.com/ember-learn/deprecation-app/pull/1408). +We should remove all references from the guides and publish the deprecation guides +written out in the Transition Path above. Those guides are tracked in these +deprecation-app PRs: + +- `Mixin`: https://github.com/ember-learn/deprecation-app/pull/1434 (originally https://github.com/ember-learn/deprecation-app/pull/1408) +- `PromiseProxyMixin`: https://github.com/ember-learn/deprecation-app/pull/1435 +- `Comparable`: https://github.com/ember-learn/deprecation-app/pull/1432 +- `Enumerable`: https://github.com/ember-learn/deprecation-app/pull/1435 +- `Observable`: https://github.com/ember-learn/deprecation-app/pull/1435 ## Drawbacks @@ -58,4 +431,4 @@ None ## Unresolved questions -None \ No newline at end of file +None