-
-
Notifications
You must be signed in to change notification settings - Fork 71
Add deprecation guides for observers #1407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need another use case for before/after:
and whatever these do:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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
setwe should tell people to handle this case by refactoring their@computedaway.There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)