Howdy,
I’m striving to understand the intricacies of tracked properties in Ember and came through the following interesting case (while rebuilding the Rock & Roll book for Octane). I’m going to show you two scenarios: both of them work but I have no idea why the second one does.
What I wanted to achieve is to have a new Band
object create and have the list of all bands instantly update with the new band. Ember Data is not used at this point.
Common parts
We have a bands
route and a bands.new
route nested inside it.
In the bands.new
controller, we create the band and add it to the catalog – and then transition back to the bands
route:
export default class BandsNewController extends Controller {
@service catalog;
@action
saveBand() {
let band = new Band({ name: this.name, slug: dasherize(this.name) })
this.catalog.add('band', band);
this.transitionToRoute('bands');
}
}
Now, how we render the list of bands and what we track in the catalog slightly differs, let’s see how.
Scenario 1
The catalog service is very similar to the Ember Data store. It looks like this:
import Service from '@ember/service';
import { tracked } from 'tracked-built-ins';
export default class CatalogService extends Service {
storage = {};
constructor() {
super(...arguments);
this.storage.bands = tracked([]);
this.storage.songs = tracked([]);
}
add(type, record) {
if (type === 'band') {
this.storage.bands.push(record);
}
if (type === 'song') {
this.storage.songs.push(record);
}
}
}
As you see, I use the excellent tracked-built-ins
add-on to mark the collections (the arrays) as tracked so any operation that mutates the arrays (like the push
in the add
method) will cause a re-computation (re-render).
We can then iterate through the collection of bands in the catalog to display the bands in bands.hbs
:
{{#each this.catalog.storage.bands as |band|}}
<li class="mb-2">
<LinkTo @route="bands.band.songs" @model={{band.slug}}>
{{band.name}}
</LinkTo>
</li>
{{/each}}
In this case, the model
hook doesn’t even need to return anything as we don’t use @model
in the template to render the bands.
When a new band is created, it gets added to the bands
array in the catalog via this.storage.bands.push(record)
. Since the array is tracked, this updates this.catalog.storage.bands
in the template, and the new band appears.
Scenario 2
We can follow a more “classic” approach and render @model
in the template:
{{#each @model as |band|}}
<li class="mb-2">
<LinkTo @route="bands.band.songs" @model={{band.slug}}>
{{band.name}}
</LinkTo>
</li>
{{/each}}
We need to then return the list of bands from the corresponding route, bands
:
export default class BandsRoute extends Route {
@service catalog;
model() {
return this.catalog.storage.bands;
}
}
We also slightly change the catalog service. We no longer track the type-specific arrays, only the storage
object itself:
import { tracked } from '@glimmer/tracking';
export default class CatalogService extends Service {
@tracked storage = {};
constructor() {
super(...arguments);
this.storage.bands = [];
this.storage.songs = [];
}
add(type, record) {
if (type === 'band') {
this.storage.bands.push(record);
}
if (type === 'song') {
this.storage.songs.push(record);
}
}
}
As you see, I also changed back to using the built-in @glimmer/tracking
. The add
method didn’t change one bit.
What’s surprising to me is that this also works. I’ve verified that the model
hook in the bands
route doesn’t get re-run. However, the @each
loop in bands.hbs
does get re-run: that’s why we see the new band appear in the list.
I don’t understand, why, though. It’s only storage
itself that’s tracked, not the bands
array inside it. storage
is not mutated, only the bands
array (when pushing a new object to it). I’m not reassigning storage
or even its bands
property anywhere. And yet it works.
Update: if I remove the this.transitionToRoute('bands')
from the saveSong
action, however, then this scenario stops working – which makes me think it’s necessary to force a re-render of bands.hbs
. However, I still don’t understand why @model
is recomputed – as it’s bound to this.catalog.storage.bands
, which we do not track.
I’m very keen to learn why it does. Thank you.