Tracked properties in non Ember classes

I’m having trouble with a non ember class not triggering renders. I have a small sample of what I’m doing here Here you can filter data by selecting one of the dynamic categories and filter inside of it. As you click the URL should update as well as the title of the button to reflect the name of what selection is being made. Only the updating URL and data filtering happens I dont understand why getters are not updating when “they should”. What could be causing these getters not not to re-run?

In this example Things work as expected. This example is way more contrived than the on up above

So I started to work my way from the contrived example to the broken version. What I found was that adding the filters dynamically is whats causing the problem.

Here is a Twiddle that has the component layers. Things are working as expected.

Here is the broken version. Where I dynamically add the filters.

The difference between the 2 is that in the latter I add the filters dynamically. The filter object reference is the same. I dont understand why is breaks?

I discussed this with @lvegerano in Discord (full convo link), the issue with the broken version comes from this snippet in controllers/application.js:

  get filterOptions() {
    console.log('buildign options')
    this.colorOption.selectedIds = [...this.colorIds];
    this.tagOption.selectedIds = [...this.tagIds];
    return [
      this.colorOption,
      this.tagOption,
    ];
  }

Note how despite the fact that this is a getter, it is mutating two values - this.colorOptions.selectedIds and this.tagOption.selectedIds. Those are what are used to render the selection in the button. However, the selection in the button is used before this getter is used in the template.

This is the issue that the backtracking rerender assertion protects against. The original twiddle is using an older version of Ember where that assertion was not working properly, which is why it’s not happening in this example.

The root of the issue, though, is really that the code is trying to synchronize two separate sources of truth. There are two arrays that represent the selected IDs, and the action only updates one of them. So, this getter is attempting to synchronize the other during render. This is a very brittle operation, because it can cause failures like this. It also leads to the possibility of having inconsistent state, if you somehow forget to sync the two.

Autotracking really is guiding us toward using a single source of truth here. We should have one value that represents the selectedIds somewhere, and all of the other state should be derived from that root state. Here is the same example but with a minor tweak to show this working in action: https://ember-twiddle.com/6bf2a95246afe527788e9cad2b8e7d12

In general, if you only have a single source of truth, autotracking will work much better. It sometimes means you have to rethink or refactor a section of code. There are also cases where you’ll want to buffer that source of truth (e.g. something like ember-changeset, where you avoid mutating the source of truth until you’re ready), and there are techniques for that. I believe ember-changeset does it currently, and I’m planning on writing out a blog post on one technique with the @localCopy decorator soon. But in general, you should always be trying to reduce your sources to a single source for every piece of state.

3 Likes