From ff77d8d42e95be726f6101287aa7e4524bf3d003 Mon Sep 17 00:00:00 2001 From: Chad Hietala Date: Sat, 22 Sep 2018 22:18:02 -0500 Subject: [PATCH 1/8] Tracked Properties --- text/0000-tracked-properties.md | 1067 +++++++++++++++++++++++++++++++ 1 file changed, 1067 insertions(+) create mode 100644 text/0000-tracked-properties.md diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md new file mode 100644 index 0000000000..ef7e21ca65 --- /dev/null +++ b/text/0000-tracked-properties.md @@ -0,0 +1,1067 @@ +- Start Date: 2018-12-05 +- RFC PR: (leave this empty) +- Ember Issue: (leave this empty) +- Authors: Tom Dale, Chris Garrett, Chad Hietala, Yehuda Katz + +# Tracked Properties + +## Summary + +Tracked properties introduce a simpler and more ergonomic system for tracking +state change in Ember applications. By taking advantage of new JavaScript +features, tracked properties allow Ember to reduce its API surface area while +producing code that is both more intuitive and less error-prone. + +This simple example shows a `Person` class with three tracked properties: + +```js +export default class Person { + @tracked firstName = 'Chad'; + @tracked lastName = 'Hietala'; + + @tracked get fullName() { + return `${this.firstName} ${this.lastName}`; + } +} +``` + +### A Note on Decorator Stability + +[Decorators](https://github.com/tc39/proposal-decorators) are important to +adopting native class syntax. They are a formalization of the patterns we have +been using as a community for years, and it will not be possible to use native +classes ergonomically without them unless a number of major concepts (computed +properties, injections, actions) are rethought. That said, decorators are still +a [_stage 2_ proposal](https://tc39.github.io/process-document) in TC39, which +means that while they are fully defined as a spec, they are not yet considered a +candidate for inclusion in the language, and may have incremental changes that +could be breaking if/when moved to stage 3. As such, merging support for them +now would pose some risk. Additionally, [class +fields](https://github.com/tc39/proposal-class-fields) are also required for +effective use of decorators, and while they are stage 3 in the process, they +have not yet been accepted either. + +This RFC proposes making the `@tracked` decorator and class fields available in +Ember for users who are comfortable taking that risk today. Ember cannot +guarantee that the spec won't change, and such changes cannot apply to Ember's +normal semver guarantees. But it can make the following guarantees: + +1. If there are changes to the spec, and it ***is*** possible to avoid changing +the public APIs of decorators, then Ember will make the changes necessary to +avoid public API changes. + +2. If there are changes to the spec, and it ***is not*** possible to avoid +breaking changes, Ember will minimize the changes as much as possible, and will +provide a codemod to convert from the previous version of the spec to the next. + +3. If the spec is dropped from TC39 altogether, Ember would have to continue to provide +support for decorators via babel transforms until they are deprecated following +the standard RFC process, and removed according to semver. Reverse codemods +which translate decorators and native class syntax back to classic class syntax +_will_ be made, and alternatives for native class syntax will be explored. + +4. Classic class syntax will continue to be supported _at least_ until these +features have been stabilized in the JavaScript language, to allow us to revert +these changes if necessary. They will most likely be supported for longer to +allow a smooth transition for users who do not want to adopt native classes +until they are completely stable. + +It is possible that decorators will be advanced before this RFC closes and this +will be a non-issue. Either way, if accepted, Ember would do the best it can to +provide the stability the framework is known for throughout this process. + +## Terminology + +Because of the occasional overlap in terminology when discussing similar +features, this document uses the following language consistently: + +- A **getter** is an ES5 JavaScript feature that executes a function to + determine the value of a property. The function is executed every time the + property is accessed. +- A **computed property** is a property on an Ember object whose value is lazily + produced by executing a function. That value is nearly always cached until one + of computed property's dependencies changes. +- A **tracked simple property** is a regular, non-getter property that has been + wrapped using the tracked decorator. +- A **tracked getter** is a JavaScript getter that has been wrapped using the + tracked decorator +- A **tracked property** refers to any property that has been instrumented with + `@tracked`, either a _tracked getter_ or a _tracked simple property_. +- The **classic programming model** refers to the traditional Ember programming + model. It includes _classic classes_, _computed properties_, _event + listeners_, _observers_, _property notifications_, and _classic components_, + and more generally refers to features that will not be central to Ember + Octane. Concepts like _routes_, _controllers_, and _services_ belong to both + the Octane programming model and the classic programming model. +- **Native classes** are classes defined using the Javascript `class` keyword. +- **Classic classes** are classes defined by subclassing from `EmberObject` + using the static `extend` method. + +## Motivation + +Tracked properties are designed to be simpler to learn, simpler to write, and +simpler to maintain than today's computed properties. In addition to clearer +code, tracked properties eliminate the most common sources of bugs and mental +model confusion in computed properties today, and reduce memory overhead by not +caching by default. + +### Leverage Existing JavaScript Knowledge + +Ember's computed properties provide functionality that overlaps with native +JavaScript getters and setters. Because native getters don't provide Ember with +the information it needs to track changes, it's not possible to use them +reliably in templates or in other computed properties. + +New learners have to "unlearn" native getters, replacing them with Ember's +computed property system. Unfortunately, this knowledge is not portable to other +applications that don't use Ember that developers may work on in the future, and +while this problem may be lessened by adopting native classes and decorators, it +still requires users learn Ember's notification system and its quirks. + +Tracked properties are as thin a layer as possible on top of native JavaScript. +Tracked properties look like normal properties because they _are_ normal +properties. + +Because there is no special syntax for retrieving a tracked property, any +JavaScript syntax that feels like it should work does work: + +```js +// Dot notation +const fullName = person.fullName; +// Destructuring +const { fullName } = person; +// Bracket notation for computed property names +const fullName = person['fullName']; +``` + +Similarly, syntax for changing properties works just as well: + +```js +// Simple assignment +this.firstName = 'Yehuda'; +// Addition assignment (+=) +this.lastName += 'Katz'; +// Increment operator +this.age++; +``` + +This compares favorably with APIs from other libraries, which becomes more +verbose than necessary when JavaScript syntax isn't available: + +```js +this.setState({ + age: this.state.age + 1, +}); +``` + +```js +this.setState({ + lastName: this.state.lastName + "Katz"; +}) +``` + +### Avoiding Dependency Hell + +Currently, Ember requires developers to manually enumerate a computed property's +dependent keys: the list of _other_ properties that _this_ computed property +depends on. Whenever one of the listed properties changes, the computed +property's cache is cleared and any listeners are notified that the computed +property has changed. + +In this example, `'firstName'` and `'lastName'` are the dependent keys of the +`fullName` computed property: + +```js +import EmberObject, { computed } from '@ember/object'; + +const Person = EmberObject.extend({ + fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.lastName}`; + }), +}); +``` + +While this system typically works well, it comes with its share of drawbacks. + +First, it's extra work to have to type every property twice: once as a string as +a dependent key, and again as a property lookup inside the function. While +explicit APIs can often lead to clearer code, this verbosity has the potential +to complicate the implementation without improving developer intent at all. +People understand intuitively that they are typing out dependent keys to help +_Ember_, not other programmers. + +Second, people tell us that this syntax is not very intuitive. You have to read +the Ember documentation at least once to understand what is happening in this +example. + +It's also not clear what syntax goes inside the dependent key string. In this +simple example it's a property name, but nested dependencies become a property +path, like `'person.firstName'`. (Good luck writing a computed property that +depends on a property with a period in the name.) + +You might form the mental model that a JavaScript expression goes inside the +string—until you encounter the `{firstName,lastName}` expansion syntax or the +magic `@each` syntax for array dependencies. + +The truth is that dependent key strings are made up of an unintuitive, +unfamiliar microsyntax that you just have to memorize if you want to use Ember +well. + +Lastly, it's easy for dependent keys to fall out of sync with the +implementation, leading to difficult-to-detect, difficult-to-troubleshoot bugs. + +For example, imagine a new member on our team is assigned a bug where a user's +middle name is not appearing in their profile. Our intrepid developer finds the +problem, and updates `fullName` to include the middle name: + +```js +import EmberObject, { computed } from '@ember/object'; + +const Person = EmberObject.extend({ + fullName: computed('firstName', 'lastName', function() { + return `${this.firstName} ${this.middleName} ${this.lastName}`; + }), +}); +``` + +They test their change and it seems to work. Unfortunately, they've just +introduced a subtle bug. If the user's `middleName` were to change, `fullName` +wouldn't update! Maybe this will get caught in a code review, given how simple +the computed property is, but noticing missing dependencies is a challenge even +for experienced Ember developers when the computed property gets more +complicated. + +Tracked properties have a feature called _autotrack_, where dependencies are +automatically detected as they are used. This means that as long as all +dependencies are marked as tracked, they will automatically be detected: + +```js +import { tracked } from '@glimmer/tracking'; + +class Person { + @tracked firstName = 'Tom'; + @tracked lastName = 'Dale'; + + @tracked + get fullName() { + return `${this.firstName} ${this.lastName}`; + } +} +``` + +This also allows us to opt out of tracking entirely, like if we know for +instance that a given property is constant and will never change. In general, +the idea is that _mutable_ properties should be marked as tracked, and +_immutable_ properties should not. + +### Reducing Memory Consumption + +By default, computed properties cache their values. This is great when a +computed property has to perform expensive work to produce its value, and that +value gets used over and over again. + +But checking, populating, and invalidating this cache comes with its own +overhead. Modern JavaScript VMs can produce highly optimized code, and in many +cases the overhead of caching is greater than the cost of simply recomputing the +value. + +Worse, cached computed property values cannot be freed by the garbage collector +until the entire object is freed. Many computed properties are accessed only +once, but because they cache by default, they take up valuable space on the heap +for no benefit. + +For example, imagine this component that checks whether the `files` property is +supported in input elements: + +```js +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + inputElement: computed(function() { + return document.createElement('input'); + }), + + supportsFiles: computed('inputElement', function() { + return 'files' in this.inputElement; + }), + + didInsertElement() { + if (this.supportsFiles) { + // do something + } else { + // do something else + } + }, +}); +``` + +This component would create and retain an `HTMLInputElement` DOM node for the +lifetime of the component, even though all we really want to cache is the +Boolean value of whether the browser supports the `files` attribute. + +Particularly on inexpensive mobile devices, where RAM is limited and often slow, we should +be more conservative about our memory consumption. Tracked properties switch +from an opt-out caching model to opt-in, allowing developers to err on the side +of reduced memory usage, but easily enabling caching (a.k.a. memoization) if a +property shows up as a bottleneck during profiling. + +## Prior Art + +Tracked properties were first implemented in [Glimmer.js](https://glimmerjs.com/), +and were recently polyfilled with clever usage of `notifyPropertyChange` by +the [sparkles-components](https://github.com/rwjblue/sparkles-component/) addon. +These initial implementations inform the design in this RFC, but differ from it +in some key ways. For instance, both Sparkles's and early versions of Glimmer's +`@tracked` did not have an autotracking stack, and instead relied on explicit +dependency keys. After benchmarking showed that autotracking was a viable +strategy, the API for `@tracked` was updated to what is proposed here. + +## Detailed Design + +This RFC proposes adding the `tracked` decorator function, used to mark +properties and getters as tracked: + +```ts +const tracked: PropertyDecorator; +``` + +This new function will be exported from `@glimmer/tracking`. Revisiting our +example from earlier, `@tracked` can be used on native class fields and +getters/setters: + +```js +import { tracked } from '@glimmer/tracking'; + +class Person { + @tracked firstName = 'Tom'; + @tracked lastName = 'Dale'; + + @tracked + get fullName() { + return `${this.firstName} ${this.lastName}`; + } +} +``` + +### Getting Tracked Properties + +Tracked properties can be accessed using standard Javascript syntax. From the +user's point of view, there is nothing special about them. This should continue +to work in the future, even if new methods are added for accessing properties, +because tracked properties use native getters under the hood. + +```js +let person = new Person(); + +// Dot notation +const fullName = person.fullName; +// Destructuring +const { fullName } = person; +// Bracket notation for computed property names +const fullName = person['fullName']; +``` + +### Setting Tracked Properties + +Tracked properties can be set using standard Javascript syntax. They use native +setters under the hood, meaning that there is no need for using a setter method +like `set`. + +```js +let person = new Person(); + +// Simple assignment +person.firstName = 'Jen'; +// Addition assignment (+=) +person.lastName += 'Weber'; +// Increment operator +person.age++; +``` + +### Autotracking + +Tracked properties do not need to specify their dependencies. Under the hood, +this works by utilizing an _autotrack stack_. This stack is a bit of global +state which tracked getters can access. As tracked getters and properties are +accessed, they push themselves onto the stack, and once they have finished +running, the stack contains the full list of all the tracked properties that +were accessed while it was running. + +In our first example, with the `Person` class, we can see this in action: + +```js +import { tracked } from '@glimmer/tracking'; + +class Person { + @tracked firstName = 'Tom'; + @tracked lastName = 'Dale'; + + @tracked + get fullName() { + return `${this.firstName} ${this.lastName}`; + } +} + +let person = new Person(); +``` + +When we create a new instance of `Person`, `fullName` has no knowledge of +`firstName` or `lastName`. If we set either of those values now, it won't know +that anything has changed: + +```js +// Dirties the `firstName` property, but because `fullName` has not been +// accessed yet nothing happens to it. +person.firstName = 'Rob'; +``` + +This is ok though, because _nothing_ has accessed `fullName` yet. There is no +state to invalidate anywhere else. Now, let's say we access `fullName`, and then +update the field again: + +```js +person.fullName; // 'Rob Dale' + +person.lastName = 'Jackson'; +``` + +Now `fullName` knows which tracked properties were accessed when it was run, +and setting `lastName` has invalidated `fullName`. + +> **NOTE:** This does _not_ invalidate a cache like in computed properties. Even +> if `firstName` and `lastName` were untracked, the tracked getter would still +> return the correct value on subsequent accesses, because `@tracked` does _not_ +> cache values. The validation and invalidation is pure meta data that is only +> accessible by the Glimmer VM. +> +> Internally, Glimmer checks to see if a value has updated _before calling the +> getter_. If it hasn't, then Glimmer does not rerender the related section of +> the DOM. This is effectively an automatic `shouldComponentUpdate` (at least the most common usage) from React. +> +> To prevent inconsistency, during development time, tracked properties will +> keep a cache of their previous value to compare when they are activated and +> ensure that it hasn't changed without invalidation. This will prevent improper +> usage of tracked properties _outside_ of Glimmer's change tracking. + +### Manual Invalidation + +In user code, the idea that all mutable properties should be marked as tracked +and that all other properties are effectively immutable works well in isolation. +However, there are cases where users will want to work with code they do _not_ +control, such as external library code. + +Consider the following example. We have a `simple-timer` library that we've +imported from NPM, and we're trying to wrap it with a `TimerComponent` that +uses it to keep track of how much time has passed: + +```js +// simple-timer/index.js +export default class Timer { + seconds = 0; + minutes = 0; + hours = 0; + + listeners = []; + + constructor() { + setInterval(() => { + this.seconds++; + this.minutes = Math.floor(this.seconds / 60); + this.hours = Math.floor(this.minutes / 60); + this.notifyTick(); + }, 1000); + } + + notifyTick() { + for (let listener of this.listeners) { + listener(this.seconds); + } + } + + onTick(listener) { + this.listeners.push(listener); + } +} +``` + +```js +import Timer from 'simple-timer'; +import Component, { tracked } from '@glimmer/tracking'; + +export default class TimerComponent extends Component { + @tracked timer = new Timer(); + + @tracked + get currentSeconds() { + return this.timer.seconds; + } + + @tracked + get currentMinutes() { + return this.timer.minutes; + } +} +``` + +Even though we've marked the `timer` property as tracked, the `timer.seconds` +property is untracked, and _it_ is the field that is updated. We can solve this +problem by using the timer library's `onTick` event handler to re-set the field, +invalidating it: + +```js +export default class TimerComponent extends Component { + @tracked timer = new Timer(); + + constructor() { + this.timer.onTick(() => { + // invalidate the timer field. + this.timer = this.timer; + }); + } + + @tracked + get currentSeconds() { + return this.timer.seconds; + } + + @tracked + get currentMinutes() { + return this.timer.minutes; + } +} +``` + +### Interop with the Classic Programming Model + +Tracked properties represent a paradigm shift. They are a completely new system, +fully independent of the classic programming model and based on modern +Javascript features and design, and they will be the _default_ change tracking +system in Ember Octane. + +However, existing apps, libraries, and addons will be using the classic +programming model for some time, and experience tells us that these sort of +transitions to new features take a while to settle in the community. To ease +this process and enable gradual adoption, tracked properties will be able to +interoperate with the most commonly used features of the classic model: + +- Classic classes +- Computed properties +- `get`/`set` and property notifications + +Tracked properties will _not_ interoperate with observers, which are strictly +within the old paradigm. + +#### Classic Classes + +The `tracked` decorator function will be usable in classic classes, similar to +`computed`: + +```js +import EmberObject from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +const Person = EmberObject.extend({ + firstName: tracked({ value: 'Tom' }), + lastName: tracked({ value: 'Dale' }), + + fullName: tracked({ + get() { + return `${this.firstName} ${this.lastName}`; + }, + }), +}); +``` + +This form will _not_ be allowed on native classes, and will hard error if it is +attempted. Additionally, default values will be defined on the _prototype_ to +maintain consistency with the classic object model. + +This will allow existing libraries to transition incrementally, and add tracked +support minimally where necessary. This also brings the _benefits_ of tracked +to classic classes, including the ability to drop usage of `set`: + +```js +// before +let person = Person.create(); +person.set('firstName', 'Stefan'); +person.set('lastName', 'Penner'); + +// after +let person = Person.create(); +person.firstName = 'Stefan'; +person.lastName = 'Penner'; +``` + +Ember's `set` function is nowhere to be seen! + +#### Computed Properties + +Computed properties will interoperate with tracked properties in both +directions: + +* Accessing a computed property from a tracked property will add the computed + property to its list of depedencies. Whenever the computed property is + invalidated (i.e. because it or one of its dependencies is updated), the + tracked property will be invalidated as well. + + ```js + import { tracked } from '@glimmer/tracking'; + import { set } from '@ember/object'; + import { alias } from '@ember/object/computed'; + + class Person { + @tracked firstName; + @tracked lastName; + + @alias('title') prefix; + + @tracked + get fullName() { + return `${this.prefix} ${this.firstName} ${this.lastName}`; + } + } + + let person = new Person(); + + person.firstName = 'Tom'; + person.lastName = 'Dale'; + + set(person, 'title', 'Mr.'); + + person.fullName; // 'Mr. Tom Dale' + ``` + +* Accessing a tracked property from a computed property will _also_ + automatically add the tracked property to the list of its dependencies. In + this way, users will be able to gradually add tracked properties and + simultaneously reap the benefits of not having to use `set` with computeds, + and not having to specify dependent keys. + + ```js + import { computed } from '@ember/object'; + import { tracked } from '@glimmer/tracking'; + + class Person { + firstName; + lastName; + + @tracked middleName; + + @computed('firstName', 'lastName') + get fullName() { + return `${this.firstName} ${this.middleName} ${this.lastName}`; + } + } + + let person = new Person(); + + set(person, 'firstName', 'Tom'); + set(person, 'lastName', 'Dale'); + + person.middleName = 'Tomster'; + + person.fullName; // 'Tom Tomster Dale' + ``` + +It will still be required to use `set` when updating computed properties and +their dependencies. In the future, this restriction could possibly be relaxed. + +#### `get` and `set` + +It is common in the classic model to set and consume plain object properties +which are not computed properties, or in any other way special. Ember's `get` +and `set` functions historically allowed this by giving us the ability to +intercept all property changes and watch for mutations. + +This presents a problem for tracked properties, particularly because of the +recent change in Ember to enable native Javascript getters to replace `get`. +This change means that we have no way to intercept `get`, and consequently no +way for tracked properties to know whether or not a plain property will later +be updated with `set`. + +To demonstrate this case, consider the following service and component: + +```js +const Config = Service.extend({ + polling: { + shouldPoll: false, + pollInterval: -1, + }, + + init() { + this._super(...arguments); + + fetch('config/api/url') + .then(r => r.json()) + .then(polling => set(this, 'polling', polling)); + }, +}); +``` +```js +class SomeComponent extends Component { + @service config; + + @tracked + get pollInterval() { + let { shouldPoll, pollInterval } = this.config.polling; + + return shouldPoll ? pollInterval : -1; + } +} +``` + +Let's walk through the flow here: + +1. The `SomeComponent` component is rendered for the first time, instantiating + the `Config` service (assuming this the first time it has ever been + accessed). The service's init hook kicks off an async request to get the + configuration from a remote URl. +2. The tracked `pollInterval` property first accesses the service injection, + which is a computed property. The property is detected and added to the + tracked stack. +3. We then access the plain, undecorated `polling` object. Because it is + is not tracked and not a computed property, tracked does not know that it + could update in the future. +4. Sometime later, the async request returns with the configuration object. We + set it on the service, but because our tracked getter did not know this + property would update, it does not invalidate. + +In order to prevent this from happening, user's will have to use `get` when +accessing any values which may be set with `set`, and are not computed +properties. + +```js +class SomeComponent extends Component { + @service config; + + @tracked + get pollInterval() { + let shouldPoll = get(this, 'config.polling.shouldPoll'); + let pollInterval = get(this, 'config.polling.pollInterval'); + + return shouldPoll ? pollInterval : -1; + } +} +``` + +The reverse, however, is not true - computed properties will be able to add +tracked properties, and listen to dependencies explicitly. In some cases, this +may be preferable, though tracked getter should be the conventional standard +with the long term goal of removing all explicit dependencies. + +#### Version Constraints + +Tracked properties should be adoptable by apps immediately since they are +backwards compatible with computed properties and prior change tracking. Addons, +however, will only be able to adopt them as soon as their minimum supported +version of Ember also supports them, otherwise their users could be left behind. + +Tracked properties will be made available for all of the 3.x LTS releases via +patch release. An additional effort will be made to backport them to the last +2.x LTS release as well. This should give addon authors some confidence that +they can update to tracked properties without dropping support for older +versions. + +## How we teach this + +There are three different aspects of tracked properties which need to be +considered for the learning story: + +1. **General usage.** Which properties should I mark as tracked? How do I + consume them? How do I trigger changes? +2. **Interop with classic systems.** How do I safely consume tracked properties + from classic classes and computeds? How do I safely consume classic APIs from + tracked properties? +3. **Interop with non-Ember systems.** How do I tell my app that something has + changed in MobX objects, RxJS objects, Redux, etc. + +### General Usage + +The mental model with tracked properties is that anything _mutable_ that is +public should be tracked. If a value will ever change, and it will or could be +watched externally, it should have the `@tracked` decorator attached to it. + +After that, usage should be "Just Javascript". You can safely access values +using any syntax you like, including desctructuring, and you can update values +using standard assignments. + +```js +// Dot notation +const fullName = person.fullName; +// Destructuring +const { fullName } = person; +// Bracket notation for computed property names +const fullName = person['fullName']; + +// Simple assignment +this.firstName = 'Yehuda'; +// Addition assignment (+=) +this.lastName += 'Katz'; +// Increment operator +this.age++; +``` + +#### Triggering Updates on Complex Objects + +There may be cases where users want to update values in complex, untracked +objects such as arrays or POJOs. `@tracked` will only be usable with class +syntax at first, and while it may make sense to formalize these objects into +tracked classes in some cases, this will not always be the case. + +To do this, users can re-set a tracked value directly after its inner values +have been updated. + +```js +class SomeComponent extends Component { + @tracked items = []; + + @action + pushItem(item) { + let { items } = this; + + items.push(item); + + this.items = items; + } +} +``` + +This may seem a bit strange at first, but it allows users to mentally scope +off a tree of objects. They manipulate internals as they see fit, and the only +operation they need to do to update state is set the nearest tracked property. + +### Interop with Classic Systems + +There are two cases that we need to consider when teaching interoperability: + +1. Tracked getters accessing non-tracked properties and computeds +2. Computed getters accessing tracked properties + +In the first case, the general rule of thumb is to use `get` if you want to be +100% safe. In cases where you are certain that the values you are accessing are +tracked, computeds, or immutable, you can safely use standard access syntax. + +In the second case, no additional changes need to be made when using tracked +properties. They can be accessed as normal, and will be automatically added to +the computed's dependencies. There is no need to use `get`, and you can use +standard assignments when updating them. + +### Interop with Non-Ember Systems + +The strategy for trickier updates on complex objects by retriggering their +setters should cover most integration use cases. We should add a guide which +specifically demonstrates their usage by wrapping a common, simple external +library such as `moment.js`. This will demonstrate its usage concretely, and +establish best practices. + +## Drawbacks + +Like any technical design, tracked properties must make tradeoffs to balance +performance, simplicity, and usability. Tracked properties make a different set +of tradeoffs than today's computed properties. + +This means tracked properties come with edge cases or "gotchas" that don't exist +in computed properties. When evaluating the following drawbacks, please consider +the two features in their totality, including computed property gotchas you have +learned to work around. + +In particular, please try to compensate for [familiarity][familiarity] and +[loss aversion][loss-aversion] biases. Before you form a strong opinion, [give +it five minutes][5-minutes]. + +[familiarity]: https://en.wikipedia.org/wiki/Familiarity_heuristic +[loss-aversion]: https://en.wikipedia.org/wiki/Loss_aversion +[5-minutes]: https://signalvnoise.com/posts/3124-give-it-five-minutes + +### Tracked Properties & Promises + +Dependency autotracking requires that tracked getters access their dependencies +synchronously. Any access that happens asynchronously will not be detected as a +dependency. + +This is most commonly encountered when trying to return a `Promise` from a +tracked getter. Here's an example that would "work" but would never update if +`firstName` or `lastName` change: + +```js +class Person { + @tracked firstName; + @tracked lastName; + + @tracked + get fullNameAsync() { + return this.reloadUser().then(() => { + return `${this.firstName} ${this.lastName}`; + }); + } + + async reloadUser() { + const response = await fetch('https://example.com/user.json'); + const { firstName, lastName } = await response.json(); + this.firstName = firstName; + this.lastName = lastName; + } + + setFirstName(firstName) { + // This should cause `fullNameAsync` to update, but doesn't, because + // firstName was not detected as a dependency. + this.firstName = firstName; + } +} +``` + +One way you could address this is to ensure that any dependencies are consumed +synchronously: + +```js +@tracked +get fullNameAsync() { + // Consume firstName and lastName so they are detected as dependencies. + let { firstName, lastName } = this; + + return this.reloadUser().then(() => { + // Fetch firstName and lastName again now that they may have been updated + let { firstName, lastName } = this; + return `${firstName} ${lastName}`; + }); +} +``` + +However, **modeling async behavior as tracked properties is an incoherent +approach and should be discouraged**. Tracked properties are intended to hold +simple state, or to derive state from data that is available synchronously. + +But asynchrony is a fact of life in web applications, so how should we deal with +async data fetching? + +**In keeping with Data Down, Actions Up, async behavior should be modeled as +methods that set tracked properties once the behavior is complete.** + +Async behavior should be explicit, not a side-effect of property access. Today's +computed properties that rely on caching to only perform async behavior when a +dependency changes are effectively reintroducing observers into the programming +model via a side channel. + +A better approach is to call a method to perform the async data fetching, then +set one or more tracked properties once the data has loaded. We can refactor the +above example back to a synchronous `fullName` tracked property: + +```js +class Person { + @tracked firstName; + @tracked lastName; + + @tracked + get fullName() { + return `${this.firstName} ${this.lastName}`; + } + + async reloadUser() { + const response = await fetch('https://example.com/user.json'); + const { firstName, lastName } = await response.json(); + this.firstName = firstName; + this.lastName = lastName; + } +} +``` + +Now, `reloadUser()` must be called explicitly, rather than being run implicitly +as a side-effect of consuming `fullName`. + +### Accidental Untracked Properties + +One of the design principles of tracked properties is that they are only +required for state that _changes over time_. Because tracked properties imply +some overhead over an untracked property (however small), we only want to pay +that cost for properties that actually change. + +However, an obvious failure mode is that some property _does_ change over time, +but the user simply forgets to annotate that property as `@tracked`. This will +cause frustrating-to-diagnose bugs where the DOM doesn't update in response to +property changes. + +Fortunately, we have a strategy for mitigating some of this frustration. It +involves the way most tracked properties will be consumed: via a component +template. In development mode, we can detect when an untracked property is used +in a template and install a setter that causes an exception to be thrown if it +is ever mutated. (This is similar to today's "mandatory setter" that causes an +exception to be thrown if a watched property is set without going through +`set()`.) + +Unfortunately this strategy cannot be applied to values accessed by tracked +getters. The only way we could detect such access would be with native +[Proxies](proxy), but proxies are more focussed on security over flexibility +and recent discussion shows that [they may break entirely when used with +private fields](https://github.com/tc39/proposal-class-fields/issues/106). As +such, it would not be ideal for us to use them in this way. + +## Alternatives + +### Ship tracked properties in user-land + +Instead of shipping `@tracked` today, we can focus on formalizing the primitives +which it uses under the hood in Glimmer VM (References and Validators) and make +these publicly consumable. This way, users will be able to implement tracked in +an addon and experiment with it before it becomes a core part of Ember. + +This approach is similar to the approach taken with component managers in the +past year, which unblocked experimentation with `SparklesComponent`s as a way to +validate the design of `GlimmerComponent`s, and unlocked the ability for power +users to create their own component APIs. However, the reference and validator +system is a much more core part of the Glimmer VM, and it could take much longer +to figure out the best and safest way to do this without exposing too much of +the internals. It would certainly prevent `@tracked` from shipping with Ember +Octane. + +### Keep the current system + +We could keep the current computed property based system, and refactor it +internally to use references only and not rely on chains or the old property +notification system. This would be difficult, since CPs are very intertwined +with property events as are their dependencies. It would also mean we wouldn't +get the DX benefits of cleaner syntax, and the performance benefits of opt-in +change tracking and caching. + +### We could keep `set` + +Tracked properties were designed around wanting to use native setters to update +state. If we remove that constraint and keep `set`, it opens up some +possibilities. There is precedent for this in other frameworks, such as React's +`setState`. + +However, keeping `set` likely wouldn't be able to restrict the requirement for +`@tracked` being applied to all mutable properties for the same reason `get` +must be used in interop - there's no way for a tracked property to know that a +plain, undecorated property could update in the future. + +### Allow explicit dependencies + +We could allow `@tracked` to receive explicit dependencies instead of forcing +`get` usage for interop. This would be very complex, if even possible, and is +ultimately not functionality `@tracked` should have in the long run, so it would +not make sense to add it now. + +### We could wait on private fields and Proxy developments + +Native [Proxies](proxy) represent a lot of possibilities for automatic change +tracking. Other frameworks such as Vue and Aurelia are looking into using +recursive proxy structures to wrap objects and intercept access, which would +allow them to track changes without _any_ decoration. We also considered using +recursive proxies in earlier drafts of this proposal, even though they aren't +part of our support matrix we believed they could be used during development to +assert when users attempted to update untracked properties which had been +consumed from tracked getters. + +However, as mention above, TC39 has made it clear that this was [not an intended +use for Proxy](https://github.com/tc39/proposal-class-fields/issues/106), and +they will be _breaking_ this functionality with the inclusion of private fields. +They have also expressed that [they would like to solve this +use-case](https://github.com/tc39/proposal-class-fields/issues/162#issuecomment-441101578) +(observing object state changes in general) separately, and [a strawman proposal +was made](https://github.com/littledan/proposal-proxy-transparent) (though it +has not advanced and does not seem like it will). We could wait to see what the +future looks like here, and see if we can provide a more ergonomic tracked +properties RFC in the future. + +[proxy]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy From 7ad52eb547771a887d1669f624c6b3276d3756eb Mon Sep 17 00:00:00 2001 From: Christopher Garrett Date: Tue, 11 Dec 2018 15:38:25 -0800 Subject: [PATCH 2/8] remove version constraints --- text/0000-tracked-properties.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md index ef7e21ca65..887b5a985d 100644 --- a/text/0000-tracked-properties.md +++ b/text/0000-tracked-properties.md @@ -750,19 +750,6 @@ tracked properties, and listen to dependencies explicitly. In some cases, this may be preferable, though tracked getter should be the conventional standard with the long term goal of removing all explicit dependencies. -#### Version Constraints - -Tracked properties should be adoptable by apps immediately since they are -backwards compatible with computed properties and prior change tracking. Addons, -however, will only be able to adopt them as soon as their minimum supported -version of Ember also supports them, otherwise their users could be left behind. - -Tracked properties will be made available for all of the 3.x LTS releases via -patch release. An additional effort will be made to backport them to the last -2.x LTS release as well. This should give addon authors some confidence that -they can update to tracked properties without dropping support for older -versions. - ## How we teach this There are three different aspects of tracked properties which need to be From cfac8edfe3bdb622bdce4b4539e025e440cd461e Mon Sep 17 00:00:00 2001 From: Christopher Garrett Date: Tue, 11 Dec 2018 15:48:34 -0800 Subject: [PATCH 3/8] update decorator stability statement --- text/0000-tracked-properties.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md index 887b5a985d..66a02c3e4d 100644 --- a/text/0000-tracked-properties.md +++ b/text/0000-tracked-properties.md @@ -56,7 +56,7 @@ provide a codemod to convert from the previous version of the spec to the next. 3. If the spec is dropped from TC39 altogether, Ember would have to continue to provide support for decorators via babel transforms until they are deprecated following -the standard RFC process, and removed according to semver. Reverse codemods +the standard RFC process, and removed according to SemVer. Reverse codemods which translate decorators and native class syntax back to classic class syntax _will_ be made, and alternatives for native class syntax will be explored. @@ -67,8 +67,9 @@ allow a smooth transition for users who do not want to adopt native classes until they are completely stable. It is possible that decorators will be advanced before this RFC closes and this -will be a non-issue. Either way, if accepted, Ember would do the best it can to -provide the stability the framework is known for throughout this process. +will be a non-issue. Either way, if accepted, Ember would maintain its strong +commitment to SemVer and making transitions to new programming models as easy as +possible. ## Terminology From dd2a1352c4c650f5eab9d22fc44c63063ea2316a Mon Sep 17 00:00:00 2001 From: Christopher Garrett Date: Tue, 11 Dec 2018 16:17:09 -0800 Subject: [PATCH 4/8] add acceptance commitments --- text/0000-tracked-properties.md | 38 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md index 66a02c3e4d..ae9c9af25e 100644 --- a/text/0000-tracked-properties.md +++ b/text/0000-tracked-properties.md @@ -47,24 +47,26 @@ guarantee that the spec won't change, and such changes cannot apply to Ember's normal semver guarantees. But it can make the following guarantees: 1. If there are changes to the spec, and it ***is*** possible to avoid changing -the public APIs of decorators, then Ember will make the changes necessary to -avoid public API changes. + the public APIs of decorators, then Ember will make the changes necessary to + avoid public API changes. 2. If there are changes to the spec, and it ***is not*** possible to avoid -breaking changes, Ember will minimize the changes as much as possible, and will -provide a codemod to convert from the previous version of the spec to the next. + breaking changes, Ember will minimize the changes as much as possible, and + will provide a codemod to convert from the previous version of the spec to + the next. -3. If the spec is dropped from TC39 altogether, Ember would have to continue to provide -support for decorators via babel transforms until they are deprecated following -the standard RFC process, and removed according to SemVer. Reverse codemods -which translate decorators and native class syntax back to classic class syntax -_will_ be made, and alternatives for native class syntax will be explored. +3. If the spec is dropped from TC39 altogether, Ember would have to continue to + provide support for decorators via babel transforms until they are deprecated + following the standard RFC process, and removed according to SemVer. Reverse + codemods which translate decorators and native class syntax back to classic + class syntax _will_ be made, and alternatives for native class syntax will be + explored. 4. Classic class syntax will continue to be supported _at least_ until these -features have been stabilized in the JavaScript language, to allow us to revert -these changes if necessary. They will most likely be supported for longer to -allow a smooth transition for users who do not want to adopt native classes -until they are completely stable. + features have been stabilized in the JavaScript language, to allow us to + revert these changes if necessary. They will most likely be supported for + longer to allow a smooth transition for users who do not want to adopt native + classes until they are completely stable. It is possible that decorators will be advanced before this RFC closes and this will be a non-issue. Either way, if accepted, Ember would maintain its strong @@ -843,6 +845,16 @@ specifically demonstrates their usage by wrapping a common, simple external library such as `moment.js`. This will demonstrate its usage concretely, and establish best practices. +## Acceptance Commitments + +> This section serves to capture the various commitments accepting this RFC +> would entail. + +* Adds support for decorators and class fields to Ember's public API. Transforms + would be included out of the box as well. +* Adds the `tracked` decorator, which can be used in both classic classes and + native classes. + ## Drawbacks Like any technical design, tracked properties must make tradeoffs to balance From 099b5f2cb322984cd9b206477f90982b9f51da50 Mon Sep 17 00:00:00 2001 From: Christopher Garrett Date: Tue, 11 Dec 2018 16:26:08 -0800 Subject: [PATCH 5/8] remove anecdotal claim --- text/0000-tracked-properties.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md index ae9c9af25e..5121e507c5 100644 --- a/text/0000-tracked-properties.md +++ b/text/0000-tracked-properties.md @@ -193,10 +193,6 @@ to complicate the implementation without improving developer intent at all. People understand intuitively that they are typing out dependent keys to help _Ember_, not other programmers. -Second, people tell us that this syntax is not very intuitive. You have to read -the Ember documentation at least once to understand what is happening in this -example. - It's also not clear what syntax goes inside the dependent key string. In this simple example it's a property name, but nested dependencies become a property path, like `'person.firstName'`. (Good luck writing a computed property that From 2232b5bb325ab222079df395fb893cef92748ea0 Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Wed, 2 Jan 2019 16:24:35 -0800 Subject: [PATCH 6/8] remove dependency on decorators --- text/0000-tracked-properties.md | 66 +++++---------------------------- 1 file changed, 9 insertions(+), 57 deletions(-) diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md index 5121e507c5..d2cf4c6725 100644 --- a/text/0000-tracked-properties.md +++ b/text/0000-tracked-properties.md @@ -25,53 +25,15 @@ export default class Person { } ``` -### A Note on Decorator Stability - -[Decorators](https://github.com/tc39/proposal-decorators) are important to -adopting native class syntax. They are a formalization of the patterns we have -been using as a community for years, and it will not be possible to use native -classes ergonomically without them unless a number of major concepts (computed -properties, injections, actions) are rethought. That said, decorators are still -a [_stage 2_ proposal](https://tc39.github.io/process-document) in TC39, which -means that while they are fully defined as a spec, they are not yet considered a -candidate for inclusion in the language, and may have incremental changes that -could be breaking if/when moved to stage 3. As such, merging support for them -now would pose some risk. Additionally, [class -fields](https://github.com/tc39/proposal-class-fields) are also required for -effective use of decorators, and while they are stage 3 in the process, they -have not yet been accepted either. - -This RFC proposes making the `@tracked` decorator and class fields available in -Ember for users who are comfortable taking that risk today. Ember cannot -guarantee that the spec won't change, and such changes cannot apply to Ember's -normal semver guarantees. But it can make the following guarantees: - -1. If there are changes to the spec, and it ***is*** possible to avoid changing - the public APIs of decorators, then Ember will make the changes necessary to - avoid public API changes. - -2. If there are changes to the spec, and it ***is not*** possible to avoid - breaking changes, Ember will minimize the changes as much as possible, and - will provide a codemod to convert from the previous version of the spec to - the next. - -3. If the spec is dropped from TC39 altogether, Ember would have to continue to - provide support for decorators via babel transforms until they are deprecated - following the standard RFC process, and removed according to SemVer. Reverse - codemods which translate decorators and native class syntax back to classic - class syntax _will_ be made, and alternatives for native class syntax will be - explored. - -4. Classic class syntax will continue to be supported _at least_ until these - features have been stabilized in the JavaScript language, to allow us to - revert these changes if necessary. They will most likely be supported for - longer to allow a smooth transition for users who do not want to adopt native - classes until they are completely stable. - -It is possible that decorators will be advanced before this RFC closes and this -will be a non-issue. Either way, if accepted, Ember would maintain its strong -commitment to SemVer and making transitions to new programming models as easy as -possible. +### A Note on Decorator Support + +This RFC proposes a decorator version of tracked properties, and uses this +decorator version in most examples, on the assumption that the [Decorators RFC] +(https://github.com/emberjs/rfcs/pull/408) will be accepted and implemented +before this RFC. If the Decorators RFC is _not_ accepted, or cannot be +implemented due to other criteria not being met (such as decorators remaining at +stage 2), then only the classic class syntax for tracked properties will be +implemented. ## Terminology @@ -841,16 +803,6 @@ specifically demonstrates their usage by wrapping a common, simple external library such as `moment.js`. This will demonstrate its usage concretely, and establish best practices. -## Acceptance Commitments - -> This section serves to capture the various commitments accepting this RFC -> would entail. - -* Adds support for decorators and class fields to Ember's public API. Transforms - would be included out of the box as well. -* Adds the `tracked` decorator, which can be used in both classic classes and - native classes. - ## Drawbacks Like any technical design, tracked properties must make tradeoffs to balance From 6612cdf396504d3a0f38eb5e6f1ba06a995a29aa Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Tue, 8 Jan 2019 10:34:36 -0800 Subject: [PATCH 7/8] Update observers, add unresolved questions --- text/0000-tracked-properties.md | 92 +++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md index d2cf4c6725..e1881df6da 100644 --- a/text/0000-tracked-properties.md +++ b/text/0000-tracked-properties.md @@ -509,9 +509,7 @@ interoperate with the most commonly used features of the classic model: - Classic classes - Computed properties - `get`/`set` and property notifications - -Tracked properties will _not_ interoperate with observers, which are strictly -within the old paradigm. +- Observers #### Classic Classes @@ -711,6 +709,61 @@ tracked properties, and listen to dependencies explicitly. In some cases, this may be preferable, though tracked getter should be the conventional standard with the long term goal of removing all explicit dependencies. +#### Observers + +While Ember's observer system has been minimized in recent years, it is still +supported in Ember 3 and used occasionally throughout the ecosystem. Observers +use a fundamentally different system for tracking changes than tracked +properties, but this does not mean that it is impossible for the two systems to +interoperate, and it theory it shouldn't require much effort to maintain such +interoperation or regress performance in any meaningful way. + +As such, tracked properties will be made to interoperate with observers so that +whenever a tracked property is set using _any_ valid syntax, observers watching +that key will be fired: + +```js +import { tracked } from '@glimmer/tracking'; +import { addObserver } from '@ember/object/observers'; + +class Person { + constructor() { + addObserver('firstName', () => { + console.log('firstName changed!'); + }); + } + + @tracked firstName; +} +``` + +If in the implementation of this RFC it becomes apparent that there _are_ major +caveats to supporting interop with observers, a followup RFC will be made to +address those caveats and make a decision on whether or not to support observers +with those additional constraints. + +### Does this mean I still have to use `get` and `set`? + +Yes. As mentioned above, interoperating with legacy code will require using +`get` and `set` to be fully safe. However, even in greenfield applications which +do not need to interoperate with legacy addons or code, there will still be use +cases which are _not_ covered by tracked properties. These use cases are roughly +the same as those that come with native [ES Getters][getters]: + +1. Objects that implement `unknownProperty` and `setUnknownProperty` +2. [Ember proxies](https://emberjs.com/api/ember/release/classes/ObjectProxy), + which use `unknownProperty` and `setUnknownProperty` +3. In general, cases where change tracking should be _dynamic_, where the keys + that are being tracked are _not_ known in advance and cannot be declared + using decorators. + +`get` and `set` will continue to work (as defined in this RFC) and will be +necessary in many applications for the forseeable future. How long exactly is +an [open question addressed below in the unresolved questions section](#unresolved-questions). + +[getters]: https://github.com/emberjs/rfcs/blob/master/text/0281-es5-getters.md#motivation + + ## How we teach this There are three different aspects of tracked properties which need to be @@ -939,7 +992,7 @@ exception to be thrown if a watched property is set without going through Unfortunately this strategy cannot be applied to values accessed by tracked getters. The only way we could detect such access would be with native -[Proxies](proxy), but proxies are more focussed on security over flexibility +[Proxies][proxy], but proxies are more focussed on security over flexibility and recent discussion shows that [they may break entirely when used with private fields](https://github.com/tc39/proposal-class-fields/issues/106). As such, it would not be ideal for us to use them in this way. @@ -992,7 +1045,7 @@ not make sense to add it now. ### We could wait on private fields and Proxy developments -Native [Proxies](proxy) represent a lot of possibilities for automatic change +Native [Proxies][proxy] represent a lot of possibilities for automatic change tracking. Other frameworks such as Vue and Aurelia are looking into using recursive proxy structures to wrap objects and intercept access, which would allow them to track changes without _any_ decoration. We also considered using @@ -1012,4 +1065,33 @@ has not advanced and does not seem like it will). We could wait to see what the future looks like here, and see if we can provide a more ergonomic tracked properties RFC in the future. +## Unresolved questions + +### When can I stop using `get` and `set`? + +This is the biggest open question in this RFC, and with the direction that +tracked properties set. How do we get rid of `get` and `set` for good, if that +is the direction we want to go in? + +The full answer to that question is out of scope for tracked properties, but it +would likely require at least two additional steps: + +1. The underlying system for tracking changes, including the ability to create + tags for fields and the ability to add to the current autotracking stack, + will need to be made public for advanced users who need dynamic change + tracking. + +2. First class support for [native proxies][proxy] within Ember. + `unknownProperty` and `setUnknownProperty` have no other analag in native + Javascript, and without support for native proxies there will likely be use + cases that cannot be supported in any other way. + + As mentioned above, native proxies _will_ (potentially) have more limitations + than Ember proxies, but these limitations will most likely be possible to + work around for advanced users who need this functionality in the first + place. In other words, while they probably don't make sense as a basis for + _all_ change tracking in Ember, they will probably be invaluable for specific + use cases such as [Ember M3](https://www.npmjs.com/package/ember-m3) which + require very dynamic change tracking. + [proxy]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy From 0147c6c63069013b801caf462a711c4d9f0b2c23 Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Fri, 11 Jan 2019 16:53:23 -0800 Subject: [PATCH 8/8] remove tracked getters --- text/0000-tracked-properties.md | 116 +++++++++++++------------------- 1 file changed, 46 insertions(+), 70 deletions(-) diff --git a/text/0000-tracked-properties.md b/text/0000-tracked-properties.md index e1881df6da..2f2951bbb5 100644 --- a/text/0000-tracked-properties.md +++ b/text/0000-tracked-properties.md @@ -19,7 +19,7 @@ export default class Person { @tracked firstName = 'Chad'; @tracked lastName = 'Hietala'; - @tracked get fullName() { + get fullName() { return `${this.firstName} ${this.lastName}`; } } @@ -46,12 +46,9 @@ features, this document uses the following language consistently: - A **computed property** is a property on an Ember object whose value is lazily produced by executing a function. That value is nearly always cached until one of computed property's dependencies changes. -- A **tracked simple property** is a regular, non-getter property that has been - wrapped using the tracked decorator. -- A **tracked getter** is a JavaScript getter that has been wrapped using the - tracked decorator -- A **tracked property** refers to any property that has been instrumented with - `@tracked`, either a _tracked getter_ or a _tracked simple property_. +- A **tracked property** refers to any class field that has been instrumented + with `@tracked`. Unlike computed properties, tracked properties are _never_ + getters or setters. - The **classic programming model** refers to the traditional Ember programming model. It includes _classic classes_, _computed properties_, _event listeners_, _observers_, _property notifications_, and _classic components_, @@ -194,7 +191,8 @@ complicated. Tracked properties have a feature called _autotrack_, where dependencies are automatically detected as they are used. This means that as long as all -dependencies are marked as tracked, they will automatically be detected: +properties that are dependencies are marked as tracked, they will automatically +be detected: ```js import { tracked } from '@glimmer/tracking'; @@ -203,17 +201,18 @@ class Person { @tracked firstName = 'Tom'; @tracked lastName = 'Dale'; - @tracked get fullName() { return `${this.firstName} ${this.lastName}`; } } ``` -This also allows us to opt out of tracking entirely, like if we know for -instance that a given property is constant and will never change. In general, -the idea is that _mutable_ properties should be marked as tracked, and -_immutable_ properties should not. +Note that getters and setters do _not_ need to be marked as tracked, only the +properties that they access need to. This also allows us to opt out of tracking +entirely, like if we know for instance that a given property is constant and +will never change. In general, the idea is that _mutable_, _watchable_ +properties should be marked as tracked, and _immutable_ or _unwatched_ +properties should not be. ### Reducing Memory Consumption @@ -280,8 +279,8 @@ strategy, the API for `@tracked` was updated to what is proposed here. ## Detailed Design -This RFC proposes adding the `tracked` decorator function, used to mark -properties and getters as tracked: +This RFC proposes adding the `tracked` decorator function, used to mark class +fields as tracked: ```ts const tracked: PropertyDecorator; @@ -298,7 +297,6 @@ class Person { @tracked firstName = 'Tom'; @tracked lastName = 'Dale'; - @tracked get fullName() { return `${this.firstName} ${this.lastName}`; } @@ -344,10 +342,10 @@ person.age++; Tracked properties do not need to specify their dependencies. Under the hood, this works by utilizing an _autotrack stack_. This stack is a bit of global -state which tracked getters can access. As tracked getters and properties are -accessed, they push themselves onto the stack, and once they have finished -running, the stack contains the full list of all the tracked properties that -were accessed while it was running. +state which tracked properties can access. As tracked properties are accessed, +they push themselves onto the stack, and once they have finished running, the +stack contains the full list of all the tracked properties that were accessed +while it was running. In our first example, with the `Person` class, we can see this in action: @@ -358,52 +356,36 @@ class Person { @tracked firstName = 'Tom'; @tracked lastName = 'Dale'; - @tracked get fullName() { return `${this.firstName} ${this.lastName}`; } } - -let person = new Person(); ``` -When we create a new instance of `Person`, `fullName` has no knowledge of -`firstName` or `lastName`. If we set either of those values now, it won't know -that anything has changed: +When we create a new instance of `Person`, the tracking system has no knowledge +of the connection between `fullName`, `firstName`, and `lastName`. Now, let's +say we go to render this person's name in a component's template: -```js -// Dirties the `firstName` property, but because `fullName` has not been -// accessed yet nothing happens to it. -person.firstName = 'Rob'; +```hbs +{{this.person.fullName}} ``` -This is ok though, because _nothing_ has accessed `fullName` yet. There is no -state to invalidate anywhere else. Now, let's say we access `fullName`, and then -update the field again: +When Glimmer accesses the `fullName` property on person, it creates an +_autotrack stack frame_. As we computed `fullName`, any values that are +decorated with `@tracked` push themselves into this stack frame. Because getters +and setters are pure functions, they will ultimately end up accessing some +tracked properties - in this case, the `fullName` getter accesses the +`firstName` and `lastName` properties, and they push themselves onto the stack +frame. -```js -person.fullName; // 'Rob Dale' +In this way, Glimmer will know about _all_ properties that were accessed when +calculating any bound value in templates. -person.lastName = 'Jackson'; -``` - -Now `fullName` knows which tracked properties were accessed when it was run, -and setting `lastName` has invalidated `fullName`. - -> **NOTE:** This does _not_ invalidate a cache like in computed properties. Even -> if `firstName` and `lastName` were untracked, the tracked getter would still -> return the correct value on subsequent accesses, because `@tracked` does _not_ -> cache values. The validation and invalidation is pure meta data that is only -> accessible by the Glimmer VM. -> +> **NOTE:** This does _not_ invalidate a cache like in computed properties. > Internally, Glimmer checks to see if a value has updated _before calling the > getter_. If it hasn't, then Glimmer does not rerender the related section of -> the DOM. This is effectively an automatic `shouldComponentUpdate` (at least the most common usage) from React. -> -> To prevent inconsistency, during development time, tracked properties will -> keep a cache of their previous value to compare when they are activated and -> ensure that it hasn't changed without invalidation. This will prevent improper -> usage of tracked properties _outside_ of Glimmer's change tracking. +> the DOM. This is effectively an automatic `shouldComponentUpdate` (at least +> the most common usage) from React. ### Manual Invalidation @@ -453,12 +435,10 @@ import Component, { tracked } from '@glimmer/tracking'; export default class TimerComponent extends Component { @tracked timer = new Timer(); - @tracked get currentSeconds() { return this.timer.seconds; } - @tracked get currentMinutes() { return this.timer.minutes; } @@ -481,12 +461,10 @@ export default class TimerComponent extends Component { }); } - @tracked get currentSeconds() { return this.timer.seconds; } - @tracked get currentMinutes() { return this.timer.minutes; } @@ -575,7 +553,6 @@ directions: @alias('title') prefix; - @tracked get fullName() { return `${this.prefix} ${this.firstName} ${this.lastName}`; } @@ -661,7 +638,6 @@ const Config = Service.extend({ class SomeComponent extends Component { @service config; - @tracked get pollInterval() { let { shouldPoll, pollInterval } = this.config.polling; @@ -669,6 +645,9 @@ class SomeComponent extends Component { } } ``` +```hbs +{{this.pollInterval}} +``` Let's walk through the flow here: @@ -676,9 +655,9 @@ Let's walk through the flow here: the `Config` service (assuming this the first time it has ever been accessed). The service's init hook kicks off an async request to get the configuration from a remote URl. -2. The tracked `pollInterval` property first accesses the service injection, - which is a computed property. The property is detected and added to the - tracked stack. +2. The `pollInterval` property first accesses the service injection when + rendered, which is a computed property. The property is detected and added to + the tracked stack. 3. We then access the plain, undecorated `polling` object. Because it is is not tracked and not a computed property, tracked does not know that it could update in the future. @@ -694,7 +673,6 @@ properties. class SomeComponent extends Component { @service config; - @tracked get pollInterval() { let shouldPoll = get(this, 'config.polling.shouldPoll'); let pollInterval = get(this, 'config.polling.pollInterval'); @@ -706,8 +684,9 @@ class SomeComponent extends Component { The reverse, however, is not true - computed properties will be able to add tracked properties, and listen to dependencies explicitly. In some cases, this -may be preferable, though tracked getter should be the conventional standard -with the long term goal of removing all explicit dependencies. +may be preferable, though undecorated getters should be the conventional +standard with the long term goal of removing all explicit dependencies and +computed decorations. #### Observers @@ -836,8 +815,8 @@ operation they need to do to update state is set the nearest tracked property. There are two cases that we need to consider when teaching interoperability: -1. Tracked getters accessing non-tracked properties and computeds -2. Computed getters accessing tracked properties +1. Accessing non-tracked properties and computeds from an autotrack context +2. Accessing tracked properties from a computed context In the first case, the general rule of thumb is to use `get` if you want to be 100% safe. In cases where you are certain that the values you are accessing are @@ -890,7 +869,6 @@ class Person { @tracked firstName; @tracked lastName; - @tracked get fullNameAsync() { return this.reloadUser().then(() => { return `${this.firstName} ${this.lastName}`; @@ -916,7 +894,6 @@ One way you could address this is to ensure that any dependencies are consumed synchronously: ```js -@tracked get fullNameAsync() { // Consume firstName and lastName so they are detected as dependencies. let { firstName, lastName } = this; @@ -953,7 +930,6 @@ class Person { @tracked firstName; @tracked lastName; - @tracked get fullName() { return `${this.firstName} ${this.lastName}`; }