@balint wrote:
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 abands.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 thebands
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 thepush
in theadd
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 viathis.storage.bands.push(record)
. Since the array is tracked, this updatesthis.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 make the catalog service simpler, not tracking 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
. Theadd
method didn’t change one bit.What’s surprising to me is that this also works. I’ve verified that the
model
hook in thebands
route doesn’t get re-run. However, the@each
loop inbands.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 thebands
array inside it.storage
is not mutated, only thebands
array (when pushing a new object to it). I’m not reassigningstorage
or even itsbands
property anywhere. And yet it works.I’m very keen to learn why that works. Thank you.
Posts: 1
Participants: 1