Let's say we have a property productName
that gets updated via user interaction. For simplicity let's make the action a simple button click, which will update the product name always to bar
:
// template.hbs
<button
type="button"
{{on "click" (fn this.updateProductName 'bar')}}
>
Update Product Name
</button>
<p>
Product name is: {{this.productName}}
</p>
<p>
Product details are: {{this.productDetails}}
</p>
In modern EmberJS (Octane) our productName
property should be marked with @tracked decorator, so that it gets updated in the template once it's value changes:
// controller.js
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class ApplicationController extends Controller {
@tracked productName = 'foo';
@action updateProductName(newValue) {
console.log(
`productName was '${this.productName}'`,
`updating to '${newValue}'`
);
this.productName = newValue;
}
}
Now let's say we want to create a derived property productDetails
(via native ES6 getter) that uses the value of productName
AND is computationally expensive. That property probably should be cached. Standard way to do this in Ember is via @cached decorator.
// controller.js
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked, cached } from '@glimmer/tracking';
export default class ApplicationController extends Controller {
@tracked productName = 'foo';
@cached
get productDetails() {
console.log(
`updating productDetails because`,
`productName was updated to '${this.productName}'`
);
// .. Some expensive computation
return `Expensive cache of ${this.productName}`;
}
@action updateProductName(newValue) {
console.log(
`productName was '${this.productName}'`,
`updating to '${newValue}'`
);
this.productName = newValue;
}
}
The problem
Now what would you expect to happen when the user clicks on the button multiple times?
I somewhat naively expected to see the "details log" to appear only once. After first click we updated productName
from foo
to bar
. Second and each subsequent click would just overwrite bar
with bar
again. Not really changing the value there. So the @tracked
does not need to be marked as dirty and @cached
would not need to get recomputed. Right?
Wrong. As it turns out @tracked
does not care what was the previous value of itself. It cares only that you tried to update it.
Edit: As Ben Demboski noted on Discord:
@tracked properties get dirtied when you write to them, period
Let's say the computation of productDetails
is really expensive and we really want to make sure it won't fire again if the value of productName
did not change. How to do that?
The solution
Manual
Well the solution is to make sure to not update productName
when it's value did not change:
@action updateProductName(newValue) {
if(this.productName !== newValue) {
this.productName = newValue;
}
}
Using an addon
As with everything in the community: If there is an itch, someone will soon build back scratcher. If you install the tracked-toolbox addon you will get a @dedupeTracked decorator, which has following description:
Turns a field in a deduped @tracked property. If you set the field to the same value as it is currently, it will not notify a property change (thus, deduping property changes). Otherwise, it is exactly the same as @tracked.
So your code would need to change only to:
// controller.js
import { dedupeTracked } from 'tracked-toolbox';
...
@dedupeTracked productName = 'foo';
...
Conclusion
- @tracked properties get dirtied when you write to them, no matter that the value is the same as the one currently stored.
- Caching is hard.
Photo by Andrea Piacquadio from Pexels
Top comments (3)
Or use github.com/pzuraq/tracked-toolbox#... (
@dedupeTracked productName = 'foo';
🙇♂️ #TIL, thank you. I adjusted the article to be more accurate & show the solution via the addon.
Interesting, thanks for writing this up! TIL.