From 5887548c4efc9dce3c5d4f4d16ca49af15a17c8d Mon Sep 17 00:00:00 2001 From: Sergey Astapov Date: Wed, 23 Mar 2022 17:06:32 -0400 Subject: [PATCH 1/6] Add `tracked-built-ins` dependency --- text/0000-tracked-built-ins.md | 287 +++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 text/0000-tracked-built-ins.md diff --git a/text/0000-tracked-built-ins.md b/text/0000-tracked-built-ins.md new file mode 100644 index 0000000000..c924fac06a --- /dev/null +++ b/text/0000-tracked-built-ins.md @@ -0,0 +1,287 @@ +--- +Stage: Accepted +Start Date: +Release Date: Unreleased +Release Versions: + ember-source: vX.Y.Z + ember-data: vX.Y.Z +Relevant Team(s): Ember CLI, Learning +RFC PR: +--- + + + +# Add tracked-built-ins + +## Summary + +This RFC proposes adding [tracked-built-ins](https://github.com/tracked-tools/tracked-built-ins) +to the blueprints that back `ember new` and `ember addon`. + +## Motivation + +Ember had historically shipped `EmberArray` and `EmberObject` +as well as `Map` and `Set` implementations to make it easy +to work with those types in a reactive way. +`tracked-built-ins` does the same for the auto-tracking era. + +Autotracking (Ember's reactivity model) is one of the key features of +[Ember Octane](https://emberjs.com/editions/octane/). `tracked-built-ins` fills the gap +in the reactivity mental model we have today when it comes to tracking updates in data structures. + +Today, `@tracked` decorator may be used to track updates of the primitive values: + +```js +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +export default class CounterComponent extends Component { + @tracked count = 0; + + @action increment() { + this.count = this.count + 1; + } + + @action decrement() { + this.count = this.count - 1; + } +} +``` + +However, there are cases when there are unknown, dynamic, or potentially infinite numbers +of decoratable keys. In such cases we can use `Object`, `Map` or another data structure +that fits most in the particular use case. The initial pattern proposed for Ember Octane +in such scenarios was to re-set the tracked property: + +```js +class TrackedMap { + @tracked map = new Map(); + + updateMapValue(key, value) { + this.map.set(key, value); + this.map = this.map; + } +} +``` + +This pattern is problematic however, because it's not obvious what the +purpose of re-setting the property is. A developer unfamiliar with Ember may +mistakenly believe this was an error, and remove this statement, causing +bugs. It's not a very google-able pattern either, which makes it more +difficult to learn and to teach. Finally, it's fairly easy this way for the +state to get out of sync with the rest of the system if a developer forgets +to add the extra re-set after mutating the value. + +Over time, it's become clear that this pattern is in actuality an antipattern +for these reasons, and something we should begin to discourage in general. + +[RFC #669 "Tracked Storage Primitives"][rfc-0669] introduced low level primitives +that allow to implement tracked versions of JavaScript's built-in classes +in an addon like [tracked-built-ins](https://github.com/tracked-tools/tracked-built-ins): + +```ts +import { + TrackedObject, + TrackedArray, + TrackedMap, + TrackedSet, + TrackedWeakMap, + TrackedWeakSet, +} from 'tracked-built-ins'; + +class Foo { + @tracked value = 123; + + obj = new TrackedObject<{[string: number]}>({ foo: 123 }); + arr = new TrackedArray(['foo', 123]); + map = new TrackedMap(['foo', 123]); + set = new TrackedSet(['foo', 123]); + weakMap = new TrackedWeakMap<{[object: number]}>([[{}, 456]]); + weakSet = new TrackedWeakSet(['foo', 123]); +} +``` + +or using `tracked()` decorator: + +```ts +import { tracked } from 'tracked-built-ins'; + +class Foo { + @tracked value = 123; + + obj = tracked<{[string: number]}>({ foo: 123 }); + arr = tracked(['foo', 123]); + map = tracked(new Map(['foo', 123])); + set = tracked(new Set(['foo', 123])); + weakMap = tracked<{[string: number]}>(new WeakMap([[{}, 456]])); + weakSet = tracked(new WeakSet(['foo', 123])); +} +``` + +## Detailed design + +The necessary changes to `ember-cli` are relatively small since we only need +to add the dependency to the `app` blueprint, and the `addon` blueprint will +inherit it automatically. + +This has the advantage (over including it as an implicit dependency), that +apps and addons that don't want to use it for some reason can opt out by +removing the dependency from their `package.json` file. + +## How we teach this + +The following guide should be added to the Autotracking In-Depth guide in the official guides, and the +sections on [POJOs][guides-pojos] and [Array][guides-arrays] should be removed. + +### Tracked data structures + +Generally, you should try to create classes with their tracked properties enumerated +and decorated with `@tracked`, instead of relying on dynamically created objects. +In some cases however, there could be a prohibitive number of possible properties, +or there could be no way to know them in advance. +In this case, you should use tracked versions of JavaScript's built-in +Plain Old JavaScript Object (POJO), array or keyed collection (Maps, Sets, WeakMaps, WeakSets) +provided by `tracked-built-ins` package. + +```js +import { + TrackedObject, + TrackedArray, + TrackedMap, + TrackedSet, + TrackedWeakMap, + TrackedWeakSet, +} from 'tracked-built-ins'; +``` + +For example, we could make a scoreboard component that keeps score for an +arbitrary number of players, and keep track of the score for each player using +tracked version of JavaScript [Map][global-objects-map]. + +**Note:** In this and the following examples, I am using `