When does @cached get recomputed in EmberJS
Tags: ember
Published: 2020-12-30 17:48:10

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`:

```hbs
// 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)](https://emberjs.com/editions/octane/) our `productName` property should be marked with [@tracked](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/#toc_updating-tracked-properties) [decorator](https://guides.emberjs.com/release/in-depth-topics/native-classes-in-depth/#toc_decorators), so that it gets updated in the template once it's value changes:

```js
// 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](https://guides.emberjs.com/release/upgrading/current-edition/native-classes/#toc_getters-and-setters)) 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](https://github.com/ember-polyfills/ember-cached-decorator-polyfill).

```js
// 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?

![DevTools console showing both logs multiple times](https://dev-to-uploads.s3.amazonaws.com/i/szpl5nrcez83p0n57cm7.png)

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](https://discord.com/channels/480462759797063690/480499624663056390/793903882056368168):

> @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:

```js
  @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](https://github.com/pzuraq/tracked-toolbox) you will get a [@dedupeTracked](https://github.com/pzuraq/tracked-toolbox#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:

```js
// 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