The rule of thumb for autotracking is that you have to explicitly track any state you want to be reactive—that is, any state you want to reflect changes to in the template.
In your specific scenario, when you just use a native array and its normal methods, that’s invisible to the reactivity system and template layer. It is, after all, just a normal array—there’s no way for Ember to know anything about it!
When you mark a property as @tracked
, you’re marking the property itself as reactive, so updates to the property will notify the reactivity system and template layer—but you’re not doing anything to the contents of the property if it’s an object, array, etc.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
class Example extends Component {
@tracked valueType = 'some string';
@tracked referenceType = [];
// THIS WORKS FINE
@action updateValueType(newValue) {
// it sets the property marked as `@tracked` directly!
this.valueType = newValue;
}
// WILL NOT WORK
@action
incorrectlyUpdateReferenceTypeContents(value) {
// doesn't do anything with `referenceType` *itself*, only
// with the things *inside* it (via the method).
this.referenceType.push(value);
}
// WILL WORK
@action
correctlyUpdateReferenceTypeContents(key, value) {
// sets `referenceType` *itself*, with an updated version of
// the array
this.referenceType = this.referenceType.concat(value);
}
}
If you want individual items within some reference type (like an array or object) to be tracked, you need to use something which can notify the system. You can either use the “functional update” style shown in the // WILL WORK
section of code there in (2), or you can use the tracked-built-ins library to get an array type which supports tracking on all the native array methods:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { TrackedArray } from 'tracked-built-ins';
import { action } from '@ember/object';
class Example extends Component {
@tracked referenceType = new TrackedArray();
// WORKS NOW!
@action
addToArray(value) {
// all the methods in `TrackedArray` notify the reactivity system
// correctly, so this works exactly as you would expect!
this.referenceType.push(value);
}
// WILL ALSO STILL WORK
@action
functionalUpdate(key, value) {
// sets `referenceType` *itself*, with an updated version of
// the array -- works because `referenceType` is still marked
// as `@tracked`
this.referenceType = this.referenceType.concat(value);
}
}
Again, the rule of thumb is: if you want a given update to be reactive, you need to explicitly make it reactive. For an individual property, you can do that by marking it as @tracked
. For something like an Array
, you need an implementation of Array
which knows how to tell the reactivity system that there has been an update when you call push
, and then forward it to the regular Array
implementation—which is exactly what TrackedArray
does.
For a deep dive into how this works “under the hood”, see my blog post on Autotracking. For examples of how to implement data structures which use autotracking correctly like TrackedArray
, see @pzuraq’s post Autotracking Case Study - TrackedMap and my post Async Data and Autotracking in Ember Octane.