More fun and games with tracked data

I am trying to understand how to carry tracking transitivity across classes, as we are trying to transform a deep nest of computational classes using @computed and @alias to the @tracked model, so I wrote a little app to zero in on the fundamentals of moving tracked data around.

application.hbs:

<h1 id="title">Adventures with Ember Tracking</h1>
<div class="left-right">
    <div>
        <h2>Data input</h2>
        <div class="in-rows">
        <label for="tracked-value">Individual Tracked Value: 
            <input type="text" placeholder="Number" id="tracked-value" value={{this.trackedValue}} {{on 'change' this.valueChanged}}>
        </label>
        <label for="tracked-container">Value In Tracked Container: 
            <input type="text" placeholder="Number" id="tracked-container" value={{this.trackedContainer.value}} {{on 'change' this.containerValueChanged}}>
        </label>
        <h3>Bumps:</h3>
        <button type="button" {{on 'click' this.bumpTrackedValue}}>Bump Tracked Value</button>
        <button type="button" {{on 'click' this.bumpValueInTrackedContainer}}>Bump Value in Tracked Container</button>
        <button type="button" {{on 'click' this.bumpTrackedValueInContainer}}>Bump Tracked Value In Container</button>
        <h3>Resets:</h3>
        <button type="button" {{on 'click' this.resetTrackedContainerFromValue}}>Reset Tracked Container Of Value From Tracked Value</button>
        <button type="button" {{on 'click' this.resetTrackedContainerFromSum}}>Reset Tracked Container Of Value From Sum</button>
        <button type="button" {{on 'click' this.resetTrackedValueInContainerFromValue}}>Reset Container Of Tracked Value From Tracked Value</button>
        <button type="button" {{on 'click' this.resetTrackedValueInContainerFromSum}}>Set Container Of Tracked Value From Sum</button>
        </div>
    </div>
    <div>
        <h2>Data Output</h2>
        <p>Individual Value: {{this.trackedValue}}</p>
        <p>Value In Tracked Container: {{this.trackedContainer.value}}</p>
        <p>Tracked Value In Container: {{this.trackedValueContainer.value}}</p>
        <p>Sneaky Value In Tracked Container: {{this.stealthValue}}</p>
        <p>Sum - getter - uses both: {{this.sum}}</p>
        <p>Sum In Tracked Container: {{this.sumTrackedContainer.value}}</p>
        <p>Tracked Sum In Container: {{this.trackedSumContainer.value}}</p>
    </div>
</div>

application.js

import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

class ValueContainer {
    constructor(value) { this.value = value;}
    value;
}

class TrackedValueContainer {
    constructor(value) { this.value = value;}
    @tracked value;
}

export default class ApplicationController extends Controller {
    @tracked trackedValue = 0;
    @tracked trackedContainer = new ValueContainer(0);
    @tracked sumTrackedContainer = new ValueContainer(0);
    trackedValueContainer = new TrackedValueContainer(this.trackedValue);
    trackedSumContainer = new TrackedValueContainer(this.sum);

    get sum() {
        return this.trackedValue + this.trackedContainer.value;
    }
    get stealthValue() {
        return this.trackedValue + this.trackedContainer.value - this.trackedValue;
    }

    @action
    valueChanged(event) {
        this.trackedValue = parseInt(event.target.value);
    }
    @action
    containerValueChanged(event) {
        this.trackedContainer = new ValueContainer(parseInt(event.target.value));
    }
    @action
    bumpTrackedValue() {
        this.trackedValue++;
    }
    @action
    bumpValueInTrackedContainer() {
        this.trackedContainer.value++;
    }
    @action
    bumpTrackedValueInContainer() {
        this.trackedValueContainer.value++;
    }
    @action
    resetTrackedContainerFromValue() {
        this.trackedContainer = new ValueContainer(this.trackedValue);
    }
    @action
    resetTrackedContainerFromSum() {
        this.sumTrackedContainer = new ValueContainer(this.sum);
    }
    @action
    resetTrackedValueInContainerFromValue() {
        this.trackedValueContainer = new TrackedValueContainer(this.trackedValue);
    }
    @action
    resetTrackedValueInContainerFromSum() {
        this.trackedSumContainer = new TrackedValueContainer(this.sum);
    }
}

app.css (in case anybody cares)

.left-right {
    display: flex;
    flex-direction: row;
    gap: 20px;
}
.in-rows {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

Some of this is no surprise. If my container is tracked but my value isn’t and I increment the value, the change happens but doesn’t show up in the browser. Neither does the viewed value of the getter that uses this value update.

  1. Click Bump Value in Tracked Container Nothing changes.
  2. Click Bump Tracked Value. The values of Sum and Sneaky Value both update with values showing indirectly that the value in the tracked container had been successfully.

This makes sense. The value isn’t tracked. Only its container is. Transitivity doesn’t extend inward. The kids don’t get into the theater for free on the parent’s ticket.

One thing that kind of makes sense, but only once I think of it, is that if I pass a constructor a tracked value or the value of an accessor that uses a tracked value, and the constructor puts it into a tracked value, tracking transitivity is broken across the construction. For instance, if I pass the sum accessor value into a constructor that puts it into a tracked member value, value won’t track changes in sum.

This also makes sense. Function parameters only carry the current value, not the place it came from. What I have done in the past is to pass the container of a tracked value, the place where it is being maintained, into a constructor and hold the reference to the container. As long as everybody is talking to the same tracked instance, everybody benefits from the tracking. You can set up a wrapper class to be used as needed for tracked values you need to pass along.

Things get dicier, though, if the thing you need to share is a getter, since you need to feed it with the things it needs, which may themselves be a long chain of getters (we may go ten deep) leading back eventually to tracked values. I think maybe we can get some leverage from careful use of closures? I don’t have this space mapped out, so this may be my next stab at research.

The third thing which really took me by surprise was that, if I tracked a value in an untracked container and then the container was re-initialized (fresh new), the UI stopped tracking the value.

  1. Click Bump Tracked Value In Container a few times and watch the Tracked Value In Container number go up.
  2. Click Reset Container Of Tracked Value From Tracked Value.
  3. Click Bump Tracked Value In Containeragain. The numbers no longer go up.

Transitivity was broken. This means that transitivity apparently doesn’t extend outward either. The reference from the HBS to the original path (this.trackedValueContainer.value) doesn’t wake up and refresh when this.trackedValueContainer is changed. Mom or Dad doesn’t get into the theater for free on the kids’ tickets either. :frowning:

To do that, I suppose the container and the value would both need to be tracked. So what I’m learning here is that changes to what isn’t explicitly tracked won’t trigger repaints, even if they are parents or children of things that are explicitly tracked. Nobody gets carried along with the tide (although I think there’s an addon or two to make it happen for the kids.)

Does all of this sound like expected stuff? Or did I mess up in my implementation somewhere, leading me to make wrong conclusions? Please advise…

Good news: you’re actually over-complicating it. There is no “transitivity” to speak of! The only thing which is reactive is… stuff you explicitly mark as tracked, either by using @tracked on a property you set, or by using a data structure which implements autotracking using the same primitives that @tracked is built on (like TrackedMap, TrackedArray, TrackedSet from tracked-built-ins, or TrackedAsyncData from ember-async-data).

If you look at every example you gave here, you can reason it through in those terms: there’s no special sauce at all. That even includes the constructor scenario: when you assign a property from a value passed into a constructor, you evaluate the property—so even if it’s a getter, you end up with the value it returns when you evaluate it, and there’s no “connection” there.

One way to help improve your reasoning about autotracking is: think about what would happen if you removed @tracked and replaced the rendering layer with just doing console.log on regular JavaScript properties. That has the exact same semantics as what @tracked and the rendering layer have: autotracking is just a careful way of notifying when a property has changed so the rendering layer can re-render the things which depend on that particular property. But everything between is 100% just JavaScript and has just normal JavaScript semantics as a result, and so you can rip out autotracking and all the same rules apply!

For some deeper dive on how it works, I recommend:

I am hoping to expand on that latter post, including with some visuals, sometime soon-ish.

1 Like

Okay, that is certainly useful. Very simple, primitive, easy to reason about, even if the underlying implementation is sophisticated. I seem to recall that the use of @computed did have some transitivity in that, if we were watching (“container.item”), it wasn’t watching items, but paths. Hence, it would kick if the values of either container or item were replaced. Is this understanding correct?

Code built to rely heavily on watching paths could afford to be a little sloppy about what level things were being updated at and will need different architectural patterns to move to using tracked.

Our computed properties always watched only the leaves we cared about. The things that changed data changed only the containers once all the leaf changes were ready. This way, we were guaranteed that all the leaves changed consistently at once and were triggered together rather than five separate changes that might get staggered by async activity. Changing things individually can leave inconsistent intermediate states to code for. Working this way, we never had to worry about which things really were joined at the hip and which things weren’t, and we never had to coordinate levels between the creators and consumers.

The lengths to which we go to avoid actually designing code. :wink: