Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions content/ember/v6/deprecate-ember-object-observable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: Deprecation of @ember/object/observable
until: 7.0.0
since: 6.5.0
---

The `get` and `set` methods from `@ember/object/observable` are deprecated. You should use native JavaScript getters and setters instead. This also applies to all built-in `Ember.Object` descendants.

### Replacing `.get()`

Instead of using `.get()`, you can now use standard property access.

**Before**

```javascript
import EmberObject from '@ember/object';

const person = EmberObject.create({
name: 'John Doe',
details: {
age: 30
}
});

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()`, you can now use standard property assignment.

**Before**

```javascript
import EmberObject from '@ember/object';

const person = EmberObject.create({
name: 'John Doe'
});

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';
```

### A Note on Legacy Computed Properties and Setters

When working with classic `EmberObject` instances, the way you set properties matters for reactivity.

#### Updating Plain Properties

To trigger reactivity (like re-computing a dependent computed property) when changing a plain property on a classic object, you **must** use the `set` function. A native JavaScript assignment (`person.firstName = 'Jane'`) will change the value but will **not** trigger reactivity.

```javascript
import { computed, set } from '@ember/object';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of telling people to use set we should tell people to handle this case by refactoring their @computed away.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a good call

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on where folks are with their migration, it could be good to have both -- the migration we have here -- but also how we would do it today using @tracked.

We could even use <details> to allow people to choose their own adventure (and not overwhelm them with a skyscraper of code blocks)

import EmberObject from '@ember/object';

const Person = EmberObject.extend({
// These properties are NOT tracked
firstName: 'John',
lastName: 'Doe',

fullName: computed('firstName', 'lastName', function() {
return `${this.get('firstName')} ${this.get('lastName')}`;
})
});

const person = Person.create();
console.log(person.fullName); // 'John Doe'

// You MUST use `set` to update the plain property for the
// computed property to react.
set(person, 'firstName', 'Jane');

console.log(person.fullName); // 'Jane Doe'
```

#### Updating Computed Properties with Setters

In contrast, if a computed property is defined with its own setter, you **can** use a native JavaScript assignment to update it. Ember will correctly intercept this and run your setter logic.

```javascript
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these examples also have the modern native class equiv? this would help AI scanning later as we want to get rid of EmberObject

import { computed } from '@ember/object';
import EmberObject from '@ember/object';

const Person = EmberObject.extend({
firstName: 'John',
lastName: 'Doe',

fullName: computed('firstName', 'lastName', {
get() {
return `${this.get('firstName')} ${this.get('lastName')}`;
},
set(key, value) {
const [firstName, lastName] = value.split(' ');
// Note: `this.set` is still used inside the setter itself
this.set('firstName', firstName);
this.set('lastName', lastName);
return value;
}
})
});

const person = Person.create();

// You CAN use a native setter on a computed property with a setter.
person.fullName = 'Jane Doe';

console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Doe'
```

However, for any properties that you use directly in a Glimmer template (`{{this.myProp}}`), you should always use `@tracked` to ensure the template updates when the property changes.
213 changes: 213 additions & 0 deletions content/ember/v6/deprecate-ember-object-observers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
---
title: Deprecation of @ember/object/observers
until: 7.0.0
since: 6.5.0
---

The `addObserver` and `removeObserver` methods from `@ember/object/observers` are deprecated. Instead of using observers, you should use tracked properties and native getters/setters.
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need another use case for before/after:

  • ember-concurrency's waitFor uses observers

and whatever these do:

  • ember-animated
  • ember-data
  • ember-power-select

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waitFor can be implemented via a rAF loop


### Before

```javascript
import EmberObject from '@ember/object';
import { addObserver, removeObserver } from '@ember/object/observers';

const Person = EmberObject.extend({
firstName: null,
lastName: null,

fullName: null,

fullNameDidChange: function() {
this.set('fullName', `${this.get('firstName')} ${this.get('lastName')}`);
}.observes('firstName', 'lastName'),
});

let person = Person.create({ firstName: 'John', lastName: 'Doe' });

addObserver(person, 'fullName', () => {
console.log('Full name changed!');
});

person.set('firstName', 'Jane');

removeObserver(person, 'fullName', null, 'fullNameDidChange');
```

### After

```javascript
import { tracked } from '@glimmer/tracking';

class Person {
@tracked firstName;
@tracked lastName;

get fullName() {
return `${this.firstName} ${this.lastName}`;
}

constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

let person = new Person('John', 'Doe');
console.log(person.fullName); // John Doe

person.firstName = 'Jane';
console.log(person.fullName); // Jane Doe
```

### Handling Observers with Side Effects

Observers are sometimes used to trigger side effects, such as logging or making a network request, when a property changes. The modern approach is to encapsulate these side effects in methods that are called explicitly.

**Before: Observer with a Side Effect**

```javascript
import EmberObject from '@ember/object';

const User = EmberObject.extend({
username: null,
lastLogin: null,

lastLoginChanged: function() {
console.log(`User ${this.get('username')} logged in at ${this.get('lastLogin')}`);
}.observes('lastLogin')
});

const user = User.create({ username: 'johndoe' });
user.set('lastLogin', new Date()); // This triggers the observer
```

**After: Explicit Method for the Side Effect**

With modern class-based components, you would create a method that updates the property and performs the side effect. This makes the code's behavior much clearer.

```javascript
import { tracked } from '@glimmer/tracking';

class User {
@tracked username;
@tracked lastLogin;

constructor(username) {
this.username = username;
}

// An explicit action that updates the property and causes the side effect
login() {
this.lastLogin = new Date();
this.logLogin();
}

logLogin() {
console.log(`User ${this.username} logged in at ${this.lastLogin}`);
}
}

const user = new User('johndoe');
user.login(); // Call the method to trigger the update and the side effect
```

### Replacing Observers with Modifiers

In legacy components, observers were often used to react to changes in component arguments (`@args`). This pattern can now be replaced with modifiers, which provide a cleaner, more reusable, and more idiomatic way to manage side effects related to DOM elements and argument changes.

**Before: Observer on a Component Argument**

Here is an example of a classic component that uses an observer to update a third-party charting library whenever its `data` argument changes.

```javascript
// Classic Component JS
import Component from '@ember/component';
import { observer } from '@ember/object';
import Chart from 'chart.js'; // A third-party library

export default Component.extend({
tagName: 'canvas',
chart: null,

// 1. Create the chart when the element is inserted
didInsertElement() {
this._super(...arguments);
this.chart = new Chart(this.element, {
type: 'bar',
data: this.get('data')
});
},

// 2. Observe the 'data' property for changes
dataDidChange: observer('data', function() {
if (this.chart) {
this.chart.data = this.get('data');
this.chart.update();
}
}),

// 3. Clean up when the component is destroyed
willDestroyElement() {
this._super(...arguments);
if (this.chart) {
this.chart.destroy();
}
}
});
```

**After: Using a Modifier**

In modern Ember, this logic can be encapsulated in a modifier. The modifier has direct access to the element and can react to argument changes, handling setup, updates, and teardown cleanly.

First, create a modifier file:

```javascript
// app/modifiers/update-chart.js
import { modifier } from 'ember-modifier';
import Chart from 'chart.js';

export default modifier(function updateChart(element, [data]) {
// This function runs whenever `data` changes.

// Get the chart instance, or create it if it doesn't exist.
// Storing the instance on the element is a common pattern.
let chart = element.chart;

if (!chart) {
// Create chart on first render
chart = new Chart(element, {
type: 'bar',
data: data
});
element.chart = chart;
} else {
// Update chart on subsequent renders
chart.data = data;
chart.update();
}

// The return function is a destructor, which handles cleanup.
return () => {
if (element.chart) {
element.chart.destroy();
element.chart = null;
}
};
});
```

Then, use this modifier in your Glimmer component's template:

```hbs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gjs?

{{! The component template }}
<canvas {{update-chart @data}}></canvas>
```

This approach is much cleaner because:
1. The logic is reusable and not tied to a specific component.
2. It clearly separates the component's data and template from the DOM-specific logic.
3. The modifier's lifecycle (setup, update, teardown) is managed by Ember, making it more robust.


Loading