DEV Community

Cover image for When does @cached get recomputed in EmberJS
Michal Bryxí
Michal Bryxí

Posted on • Edited on

When does @cached get recomputed in EmberJS

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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?

DevTools console showing both logs multiple times

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;
    }
  }
Enter fullscreen mode Exit fullscreen mode

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';

...
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
bendemboski profile image
Ben Demboski

Or use github.com/pzuraq/tracked-toolbox#... (@dedupeTracked productName = 'foo';

Collapse
 
michalbryxi profile image
Michal Bryxí

🙇‍♂️ #TIL, thank you. I adjusted the article to be more accurate & show the solution via the addon.

Collapse
 
iamdtang profile image
David Tang

Interesting, thanks for writing this up! TIL.