emberjs / emberjs/rfcs

Thoughts on this more ergonomic way to wire up the owner + destroyable association?

Open
#905 3 comments 9 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
801
Forks
409
PR merge metrics
No merged PRs in 30d

Description

## Problem

there is a lot of boilerplate for creating vanilla classes that are properly wired up to the container.
For example:
```js
import { cached } from '@glimmer/tracking';
import { getOwner, setOwner } from '@ember/owner';
import { associateDestroyableChild } from '@ember/destroyable';

class MyClass { /* ... */ }

export default class Demo extends Component {
@cached
get myInstance() {
let instance = new MyClass();

associateDestroyableChild(this, instance);

let owner = getOwner(this);

if (owner) {
setOwner(instance, owner);
}

return instance;
}
}
```

## Solution

this would be a new feature to ember in my ideal world, and not in a user-land package (even though it _could_ live there)

Usage:
```ts
import { link } from '@ember/owner'; // proposed;

class MyClass { /* ... */ }

export default class Demo extends Component {
@link myInstance = new MyClass();

// alternatively, suggestion by Runspired
@link(MyClass) declare myInstance: MyClass;
}
```

- One import instead of 3
- Two lines instead of 13
- This is a common task -- and something that's easy to mess up as the order of params swaps between `associateDestroyableChild` and `setOwner`

Implementation:

```ts
function link(_prototype: object, key: string, descriptor?: Descriptor): void {
if (!descriptor) return;

assert(`@link can only be used with string-keys`, typeof key === 'string');

let { initializer } = descriptor;

assert(
`@link may only be used on initialized properties. For example, ` +
`\`@link foo = new MyClass();\``,
initializer
);

let caches = new WeakMap();

// https://github.com/pzuraq/ember-could-get-used-to-this/blob/master/addon/index.js
return {
get(this: object) {
let child = caches.get(this);

if (!child) {
child = initializer.call(this);

associateDestroyableChild(this, child);

let owner = getOwner(this);

if (owner) {
setOwner(child, owner);
}

caches.set(this, child);
assert(`Failed to create cache for internal resource configuration object`, child);
}

return child;
},
} as unknown as void /* Thanks TS. */;
}
```
Disclaimer: I've given up with "properly typing" Stage 1 decorators, and instead `assert` my way in to the shapes I need.

Thoughts?

----------------

Some may wonder how you'd reactively manage args passed to `MyClass`, and the answer there is a Resource.
Reason being is that you can't use this pattern:
```js
@cached
get myInstance() {
let instance = new MyClass(this.args.foo);

link(this, instance); // overloaded

return instance;
}
```
because you only get constructions of `MyClass`, and no destructions or updates when the args change.
This may be perfectly fine for some cases, but Resources are specifically for managing this destruction-or-update scenario where as the `@cached` getter is constructions only, until the parent class is destroyed (then all relevant destructors still remaining in memory would be called)
Sure, you could manage an "update" pattern via a local un-tracked variable such as:
```js
_myInstance;

@cached
get myInstance() {
if (this._myInstance) {
this._myInstance.update(this.args.foo);
return this._myInstance;
}
let instance = new MyClass(this.args.foo);

link(this, instance); // overloaded

this._myInstance = instance;
return instance;
}
```
But wouldn't it feel much better to do this instead:
```js
@use myInstance = MyClass.from(() => [this.args.foo]);
// or
myInstance = MyClass.from(this, () => [this.args.foo]);
```
ember-resources supports both of these APIs (with and without the decorator), and both are 100% TS safe / make sense to consumers. I need more feedback before one could be decided on -- and no matter what happens, there will be codemods <3

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.