diff --git a/content/ember/v6/deprecate-array-proxy.md b/content/ember/v6/deprecate-array-proxy.md new file mode 100644 index 00000000..d73625ac --- /dev/null +++ b/content/ember/v6/deprecate-array-proxy.md @@ -0,0 +1,275 @@ +--- +title: Deprecation of ArrayProxy +until: 7.0.0 +since: 6.5.0 +--- + + +`ArrayProxy` is deprecated. In modern Ember, you should use tracking primitives—such as tracked arrays and tracked properties—whenever possible. This is almost always the best approach going forward. Some of the examples below demonstrate alternatives using proxies or advanced patterns, but these are generally not ideal. You should strongly consider refactoring your code to use tracking if at all possible, as it leads to simpler, more maintainable, and idiomatic Ember code. However, the best replacement depends on how you were using `ArrayProxy`. Some example use cases are shown below. + +## Recommended: Use Tracked Arrays + +For most use cases, the modern Ember approach is to use tracked arrays from [`tracked-built-ins`](https://github.com/tracked-tools/tracked-built-ins): + +```javascript +import { TrackedArray } from 'tracked-built-ins'; + +// Instead of ArrayProxy, use TrackedArray directly +const pets = new TrackedArray(['dog', 'cat', 'fish']); + +// The array is automatically tracked and will update templates +pets.push('bird'); +pets[0]; // 'dog' +pets.length; // 4 +``` + +This provides automatic tracking without the complexity of proxies and follows modern Ember patterns. + + +## Advanced Use Cases + +If you need more advanced behavior like content swapping or transformation, you can use the approaches below. However, these patterns are generally not recommended unless you have a strong reason not to use tracked arrays and properties. In most cases, refactoring to use tracking primitives will result in better, more future-proof code. + +### Swapping Content + + +If you were using `ArrayProxy` to easily swap out the underlying array while keeping a stable reference, you can achieve a similar, transparent effect using a native `Proxy` backed by a class with a `@tracked` property. Again, prefer tracked arrays and properties if you can refactor your code to use them. + +Before: + +```javascript +import ArrayProxy from '@ember/array/proxy'; +import { A } from '@ember/array'; + +const pets = A(['dog', 'cat', 'fish']); +const proxy = ArrayProxy.create({ content: pets }); + +proxy.get('firstObject'); // 'dog' + +// Later, you can easily swap the content +proxy.set('content', A(['amoeba', 'paramecium'])); + +proxy.get('firstObject'); // 'amoeba' +``` + +After: + +```javascript +import { tracked } from '@glimmer/tracking'; + +// A helper class to hold the tracked state. +class SwappableState { + @tracked content; + + constructor(initialContent) { + this.content = initialContent; + } +} + +// A factory function to create a proxy that is transparent +// and allows swapping the underlying content. +function createSwappableArray(initialContent) { + const state = new SwappableState(initialContent); + + return new Proxy(state, { + get(target, property, receiver) { + // Allow getting/setting the content directly for swapping. + if (property === 'content') { + return target.content; + } + // Delegate all other property access to the content array. + return Reflect.get(target.content, property, receiver); + }, + set(target, property, value, receiver) { + // Allow setting the content directly for swapping. + if (property === 'content') { + target.content = value; + return true; + } + // Delegate all other property sets to the content array. + return Reflect.set(target.content, property, value, receiver); + }, + // Add other traps to make the proxy behave like a full array. + has: (target, key) => key in target.content, + ownKeys: (target) => Reflect.ownKeys(target.content), + getOwnPropertyDescriptor: (target, key) => Reflect.getOwnPropertyDescriptor(target.content, key), + defineProperty: (target, key, desc) => Reflect.defineProperty(target.content, key, desc), + deleteProperty: (target, key) => Reflect.deleteProperty(target.content, key), +}); +} + +const pets = createSwappableArray(['dog', 'cat', 'fish']); + +// Access the array transparently using native syntax. +pets[0]; // 'dog' +pets.length; // 3 + +// Later, you can easily swap the content. +// Any part of your app observing this will update because +// the underlying state is tracked. +pets.content = ['amoeba', 'paramecium']; + +pets[0]; // 'amoeba' +pets.length; // 2 +``` + +### Transforming Content + + +If you were using `objectAtContent` to transform the array's content, you can use a native JavaScript `Proxy` to achieve the same result with standard array syntax. This is an advanced pattern and should only be used if refactoring to tracked properties is not feasible. + +Before: + +```javascript +import ArrayProxy from '@ember/array/proxy'; + +let pets = ['dog', 'cat', 'fish']; +let proxy = ArrayProxy.create({ + content: pets, + objectAtContent(idx) { + return this.get('content').objectAt(idx).toUpperCase(); + } +}); + +proxy.get('firstObject'); // 'DOG' +proxy.objectAt(1); // 'CAT' +``` + +After: + +```javascript +const pets = ['dog', 'cat', 'fish']; + +const transformedPets = new Proxy(pets, { + get(target, property, receiver) { + // Check if the property is an array index. + if (typeof property === 'string' && /^\d+$/.test(property)) { + const index = parseInt(property, 10); + const value = target[index]; + return typeof value === 'string' ? value.toUpperCase() : value; + } + + // For other properties like 'length', delegate to the original array. + return Reflect.get(target, property, receiver); + } +}); + +// Now you can access the transformed items using native array syntax. +transformedPets[0]; // 'DOG' +transformedPets[1]; // 'CAT' + +// Other array properties work as expected. +transformedPets.length; // 3 +``` + +### Sorted or Filtered Content (`arrangedContent`) + +If you were using `arrangedContent` to provide a sorted or filtered view of an array, the modern approach is to use tracked properties and getters: + +Before: + +```javascript +import ArrayProxy from '@ember/array/proxy'; +import { computed } from '@ember/object'; +import { A } from '@ember/array'; + +const people = A([{name: 'Yehuda'}, {name: 'Tom'}]); + +const proxy = ArrayProxy.extend({ + arrangedContent: computed('content.[]', function() { + // In classic Ember, `sortBy` was a common way to do this. + return this.get('content').sortBy('name'); + }) +}).create({ content: people }); + +proxy.get('arrangedContent.firstObject.name'); // 'Tom' + +// Mutating the content... +people.pushObject({ name: 'Chris' }); + +// ...is reflected in arrangedContent. +proxy.get('arrangedContent.firstObject.name'); // 'Chris' +``` + +After (modern Ember approach with tracked properties): + +```javascript +import { TrackedArray } from 'tracked-built-ins'; +import { cached } from '@glimmer/tracking'; + +class PeopleManager { + // Use TrackedArray for automatic reactivity + people = new TrackedArray([{name: 'Yehuda'}, {name: 'Tom'}]); + + @cached + get arrangedContent() { + // Automatically recomputes when people array changes + return [...this.people].sort((a, b) => a.name.localeCompare(b.name)); + } +} + +const manager = new PeopleManager(); +manager.arrangedContent[0].name; // 'Tom' + +// Mutating the content... +manager.people.push({ name: 'Chris' }); + +// ...is reflected in arrangedContent due to @cached and TrackedArray. +manager.arrangedContent[0].name; // 'Chris' +``` + + +For more complex use cases where you need a native `Proxy` for dynamic behavior, you can use the following pattern. However, this is rarely necessary and should be avoided if you can use tracked properties and computed values instead: + +```javascript +// The original data, which can be mutated. +const people = [{name: 'Yehuda'}, {name: 'Tom'}]; + +// A cache for the sorted version. +let sortedCache = null; +let isDirty = true; + +const peopleProxy = new Proxy(people, { + get(target, property, receiver) { + // Intercept access to a special 'arranged' property. + if (property === 'arranged') { + if (isDirty) { + // The cache is dirty, so we re-compute the sorted array. + sortedCache = [...target].sort((a, b) => a.name.localeCompare(b.name)); + isDirty = false; + } + return sortedCache; + } + + // For any other property, delegate to the original array. + return Reflect.get(target, property, receiver); + }, + set(target, property, value, receiver) { + // Any mutation to the array marks the cache as dirty. + isDirty = true; + return Reflect.set(target, property, value, receiver); + } +}); + +// Access the sorted content via the `arranged` property. +peopleProxy.arranged[0].name; // 'Tom' + +// Mutate the original data through the proxy. +peopleProxy.push({ name: 'Chris' }); + +// The `arranged` property now reflects the change because the cache was invalidated. +peopleProxy.arranged[0].name; // 'Chris' +``` + +## Migration Strategy + +> Warning: Any ComputedProperties that depend on ArrayProxy values may behave differently after removing ArrayProxy. Please consider +> migrating to native getters first. + +When migrating from `ArrayProxy`, consider: + +1. **First choice (strongly recommended)**: Use `TrackedArray` from `tracked-built-ins` and tracked properties for automatic reactivity and idiomatic Ember code. +2. **For computed arrays**: Use `@cached` getters with tracked data. +3. **Only if truly necessary**: Use native `Proxy` for complex dynamic behavior that cannot be achieved with tracked properties. This should be rare. + +The modern Ember approach strongly favors explicit tracking and computed properties over proxy-based solutions. Tracking primitives are easier to understand, debug, and optimize, and will be the best choice for almost all use cases going forward. diff --git a/content/ember/v6/deprecate-object-proxy.md b/content/ember/v6/deprecate-object-proxy.md new file mode 100644 index 00000000..70f8a36f --- /dev/null +++ b/content/ember/v6/deprecate-object-proxy.md @@ -0,0 +1,217 @@ +--- +title: Deprecation of ObjectProxy +until: 7.0.0 +since: 6.5.0 +--- + +`ObjectProxy` is deprecated. In modern Ember, you should use tracked properties and native JavaScript patterns instead. The best replacement depends on how you were using `ObjectProxy`. Some example use cases are shown below. + +## Recommended: Use Tracked Properties + +For most use cases, the modern Ember approach is to use tracked properties directly: + +```javascript +import { tracked } from '@glimmer/tracking'; + +class PersonManager { + @tracked person = { name: 'Tom' }; + + // Easy to swap content and templates will update automatically + updatePerson(newPerson) { + this.person = newPerson; + } +} +``` + +This provides automatic tracking without the complexity of proxies and follows modern Ember patterns. + +## Advanced Use Cases + +If you need more advanced behavior like content swapping with a stable reference or property interception, you can use the approaches below. + +### Swapping Content + +If you were using `ObjectProxy` to easily swap out the underlying object while keeping a stable reference, you can achieve a similar, transparent effect using a native `Proxy` backed by a class with a `@tracked` property. + +Before: + +```javascript +import ObjectProxy from '@ember/object/proxy'; + +const person = { name: 'Tom' }; +const proxy = ObjectProxy.create({ content: person }); + +proxy.get('name'); // 'Tom' + +// Later, you can easily swap the content +proxy.set('content', { name: 'Thomas' }); + +proxy.get('name'); // 'Thomas' +``` + +After: + +```javascript +import { tracked } from '@glimmer/tracking'; + +// A helper class to hold the tracked state. +class SwappableState { + @tracked content; + + constructor(initialContent) { + this.content = initialContent; + } +} + +// A factory function to create a proxy that is transparent +// and allows swapping the underlying content. +function createSwappableObject(initialContent) { + const state = new SwappableState(initialContent); + + return new Proxy(state, { + get(target, property, receiver) { + // Allow getting/setting the content directly for swapping. + if (property === 'content') { + return target.content; + } + // Delegate all other property access to the content object. + return Reflect.get(target.content, property, receiver); + }, + set(target, property, value, receiver) { + // Allow setting the content directly for swapping. + if (property === 'content') { + target.content = value; + return true; + } + // Delegate all other property sets to the content object. + return Reflect.set(target.content, property, value, receiver); + }, + // Add other traps to make the proxy behave like a full object. + has: (target, key) => key in target.content, + ownKeys: (target) => Reflect.ownKeys(target.content), + getOwnPropertyDescriptor: (target, key) => Reflect.getOwnPropertyDescriptor(target.content, key), + defineProperty: (target, key, desc) => Reflect.defineProperty(target.content, key, desc), + deleteProperty: (target, key) => Reflect.deleteProperty(target.content, key), + }); +} + +const person = createSwappableObject({ name: 'Tom' }); + +// Access properties transparently. +person.name; // 'Tom' + +// Later, you can easily swap the content. +// Any part of your app observing this will update because +// the underlying state is tracked. +person.content = { name: 'Thomas' }; + +person.name; // 'Thomas' +``` + +### Adding/Overriding Properties + +If you had computed properties on your proxy or were using it to add or override behavior, you often don't need a proxy at all. You can simply add a getter to the object: + +Before: + +```javascript +import ObjectProxy from '@ember/object/proxy'; +import { computed } from '@ember/object'; + +const ProxyWithComputedProperty = ObjectProxy.extend({ + fullName: computed('firstName', 'lastName', function() { + return `${this.get('firstName')} ${this.get('lastName')}`; + }) +}); + +let proxy = ProxyWithComputedProperty.create({ + content: { firstName: 'Tom', lastName: 'Dale' } +}); + +proxy.get('fullName'); // 'Tom Dale' +``` + +After: + +```javascript +const person = { + firstName: 'Tom', + lastName: 'Dale', + get fullName() { + // or person.firstName person.lastName + return `${this.firstName} ${this.lastName}`; + } +}; + +person.fullName; // 'Tom Dale' +person.firstName; // 'Tom' +``` + +If you need to add properties to an object you can't modify, you can use a native `Proxy`: + +```javascript +const person = { firstName: 'Tom', lastName: 'Dale' }; + +const personProxy = new Proxy(person, { + get(target, property, receiver) { + if (property === 'fullName') { + return `${target.firstName} ${target.lastName}`; + } + return Reflect.get(target, property, receiver); + } +}); + +personProxy.fullName; // 'Tom Dale' +personProxy.firstName; // 'Tom' +``` + +### Handling Unknown Properties (`unknownProperty`) + +For more advanced use cases, `ObjectProxy` provided an `unknownProperty` hook to handle access to properties that don't exist. The `get` trap in a native `Proxy` provides the same capability. + +Before: + +```javascript +import ObjectProxy from '@ember/object/proxy'; + +const proxy = ObjectProxy.extend({ + unknownProperty(key) { + return `Property '${key}' does not exist.`; + } +}).create({ + content: { a: 1 } +}); + +proxy.get('a'); // 1 +proxy.get('b'); // "Property 'b' does not exist." +``` + +After: + +```javascript +const data = { a: 1 }; + +const handler = { + get(target, key, receiver) { + if (key in target) { + return Reflect.get(target, key, receiver); + } + return `Property '${key}' does not exist.`; + } +}; + +const proxy = new Proxy(data, handler); + +proxy.a; // 1 +proxy.b; // "Property 'b' does not exist." +``` + +## Migration Strategy + +When migrating from `ObjectProxy`, consider: + +1. **First choice**: Use `@tracked` properties and direct object access +2. **For computed properties**: Add getters directly to objects when possible +3. **Only if needed**: Use native `Proxy` for dynamic property access or when you can't modify the original object + +The modern Ember approach favors explicit tracked properties and direct object access over proxy-based solutions, which are easier to understand, debug, and have better performance characteristics. diff --git a/content/ember/v6/deprecate-promise-proxy-mixin.md b/content/ember/v6/deprecate-promise-proxy-mixin.md new file mode 100644 index 00000000..420f3f99 --- /dev/null +++ b/content/ember/v6/deprecate-promise-proxy-mixin.md @@ -0,0 +1,124 @@ +--- +title: Deprecation of PromiseProxyMixin +until: 7.0.0 +since: 6.5.0 +--- + +`PromiseProxyMixin` is deprecated. You should use native `async/await` and Promises directly to manage asynchronous operations and their state. + +`PromiseProxyMixin` was used to create proxy objects that represented the eventual result of a promise, with properties to track the promise's lifecycle (`isPending`, `isFulfilled`, etc.). + +### Replacing `PromiseProxyMixin` + +The modern approach is to use a class (like a component or service) to manage the state of the asynchronous operation. + +Before: + +```javascript +import ObjectProxy from '@ember/object/proxy'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; + +const PromiseObject = ObjectProxy.extend(PromiseProxyMixin); + +const promise = new Promise(resolve => resolve({ value: 42 })); +const proxy = PromiseObject.create({ promise }); + +// In a template, you might have: +// {{#if proxy.isPending}} +// Loading... +// {{else if proxy.isFulfilled}} +// Value: {{proxy.content.value}} +// {{else if proxy.isRejected}} +// Error: {{proxy.reason}} +// {{/if}} +``` + +After (using `async/await` and tracked properties in a component): + +```gjs +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; + +export default class MyComponent extends Component { + @task + *loadData() { + try { + const promise = new Promise(resolve => resolve({ value: 42 })); + const content = yield promise; + return content; + } catch (e) { + // ember-concurrency provides its own error state + throw e; + } + } + + get lastTask() { + return this.loadData.last; + } + + +} +``` + +For simpler cases where you don't need the full power of a library like `ember-concurrency`, you can manage the state manually with `@tracked` properties: + +```gjs +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +export default class MyComponent extends Component { + @tracked isLoading = true; + @tracked error = null; + @tracked content = null; + + constructor() { + super(...arguments); + this.loadData(); + } + + async loadData() { + try { + this.isLoading = true; + const promise = new Promise(resolve => resolve({ value: 42 })); + this.content = await promise; + } catch (e) { + this.error = e; + } finally { + this.isLoading = false; + } + } + + +} +``` + +Using a library like [ember-concurrency](https://ember-concurrency.com/docs/introduction) is highly recommended for managing concurrent asynchronous user-initiated tasks in Ember applications, as it provides robust solutions for handling loading/error states, cancellation, and more. + +For data loading specifically, you may also want to consider using [WarpDrive](https://warp-drive.io) (formerly Ember Data) which provides a number of utilities around tracking for data. + +## Migration Strategy + +When migrating from `PromiseProxyMixin`, consider: + +1. **First choice**: Use `ember-concurrency` for user-initiated async tasks (button clicks, form submissions) +2. **For data loading**: Consider `getRequestState` from warp-drive for request state management +3. **For simple cases**: Use `@tracked` properties with `async/await` and manual state management + +The modern Ember approach uses explicit async/await patterns and proper state management libraries rather than proxy-based promise wrappers.