Using DDAU with @tracked when UI itself has multiple sources of truth

I am running into situations where the user has multiple alternative ways of entering data on a form that don’t seem to track well for the pattern of DDAU / getters / @tracked / handlebar fields alone.

I will start with two cases where, with related values, while A is the source of truth, B is derived, and while B is the source of truth, A is derived. Truth originates from whichever value was the most recent source of user input.

Case 1: Suppose I have a UI for [Base Value] x [Multiplier] = [Final Value], where all three items in braces are input boxes. User entry is “push/pull” between them with the following rules:

  • Initially, a default base value and a multiplier of 1 are used to calculate a final value.
  • If the user enters a new base value, the existing multiplier is used and a new final value is produced.
  • If the user enters a new multiplier, it is multiplied by the base value and a new final value is produced.
  • If the user enters a new final value, it is divided by the multiplier and a new base value is produced.

So an update to any of them will update itself and one of the others, while leaving the third value alone, but none of the three is purely a derived value, since it can be edited. I’m trying to figure a scenario using only actions that set values, tracked values, getters, and values in handlebars - no property setters - that will support this scenario.

Case 2: Related values with near-equivalence

Suppose I have a value, say a frequency, and a discrete counter value that, when applied to a mechanism, produces a frequency. Since the counter in the mechanism has a limited number of bits, it may only be able to deliver a frequency close to a desired frequency. The user can either:

  • edit the frequency, and the app calculates the counter value that will produce the closest available frequency to the one asked for (within the bit limitations of the counter), or * edit the counter value, and the app produces the frequency (to the accuracy representable by a 64-bit float) that counter will produce.

Which is exact and which is an approximation depends upon which the user supplied last. At any given moment, one or the other is the true “source of truth”.

In both of these cases, the designation of which is the source of truth at the moment is in some ways part of the state of the model. What is the cleanest way to represent this in Octane?

I don’t think you have multiple sources of truth. A derived state could be writeable. In that case setting the derived state will cause the root state to be updated accordingly. Technically speaking it has not only a getter but a getter and setter.

For your first case you could treat the base value and multiplexer as the root state and final value as derived state. In that case it could be expressed like this:

import { tracked } from '@glimmer/tracking';

class MyComponent extends Component {
  @tracked baseValue = 10;
  @tracked multiplier = 1;

  get finalValue() {
    return this.baseValue * this.multiplier;
  }
  set finalValue(finalValue) {
    this.baseValue = finalValue / this.multiplier;
  }
}

All of the three values can be rendered to and mutated by the user. But it is nevertheless only two root states and one derived state. A template may be implemented like this using set helper from ember-set-helper and pick helper from ember-composable-helpers looks like this:

<input value={{this.baseValue}} {{on "change" (pick "target.value" (set this "baseValue"))>
*
<input value={{this.multiplier}} {{on "change" (pick "target.value" (set this "multiplier"))>
=
<input value={{this.finalValue}} {{on "change" (pick "target.value" (set this "finalValue"))>

For your second case I’m not sure if you would consider the information if either discrete counter or frequency is set as part of your root state. Especially as some information is lost when converting from one to the other. Usually this could be answered by looking at the data model. Whatever is persisted through a REST API or some other mechanism should be considered the root state.

  1. If you do not need to store the information, which of the two values was entered by the user, you could model it the same as the first case. E.g. the discrete counter may be the root state and the frequency is a derived state, which could be written to.
  2. If not, neither of the two seems to be the root state. Instead the root state consists of the both a value and the unit of the value, which would be either discrete counter or frequency. Both the discrete counter and frequency would be derived states in that case.

In all cases the main idea is that a derived state is not limited to be read-only. It could also provide a setter, which updates the root state when ever the derived state is set.

@jelhan’s answer works, so if you quite like it feel free to run with it—but I disagree with the advice quite strongly, and recommend a very different approach!

The rest of this answer has two parts: a concrete recommendation about how to solve these problems, and then a set of heuristics I use to think about these things, which provide the reasoning for why these are my recommendations. I strongly advise reading both! (I’m also going to extract these heuristics and work to get them integrated into a set of heuristics I can publish elsewhere as a more general resource.)

Recommendation

In both of these cases, I would actually take a step back and think about what your root state actually is, and then update your actions accordingly.

In Case 1, it’s clear from your description that you want to reach for getters because your mental model here is that the states are derived from each other. But I don’t think they are. It’s rather that there is a relationship between them. All of them are root state, but the root state is actually the composition of some related pieces of data: the base value, the multiplier, and the combination of the two into a final value. My solution here would be to lean into that, and to treat them as a coherent set of top-level state. Then your updates to that top-level state can be in terms of a single “pure” function, where you pass in the previous state and an update and produce a new state. Here’s how that might look (using TS to clarify the data relationships, but of course you can do exactly the same in normal JS):

interface Data {
  base: number;
  multiplier: number;
  final: number;
}

class UI extends Component {
  @tracked data: Data = {
    base: 1,
    multipler: 1,
    final: 1,
  };

  @action update(type: keyof Data, value: number) {
    // see below!
    this.data = update(type, value, this.data);
  }
}

// Given the previous `Data` and a specific value to update, produce new `Data`
function update(type: keyof Data, value: number, prev: Data): Data {
  switch(type) {
    case 'base':
      return {
        base: value,
        multipler: prev.multiplier,
        final: base * prev.multiplier,
      };
    case 'multiplier':
      return {
        base: prev.base,
        multipler: value,
        final: prev.base * value,
      };
    case 'final':
      return {
        base: value / prev.multiplier,
        multipler: prev.multiplier,
        final: value,
      };
    default:
      throw new Error(`bad type "${type}" for update`);
  }
}

At a practical level, you could equally do that with three separate actions on the backing class and/or three standalone functions to compute the update (updateBase, updateMultiplier, and updateFinal), but I like this approach, because it keeps together the things which change together. The template ends up just looking like this:

<input
  value={{this.data.base}}
  {{on "change" (pick "target.value" (fn this.update "base"))}}
>
*
<input
  value={{this.data.multiplier}}
  {{on "change" (pick "target.value" (fn this.update "multiplier"))}}
>
=
<input
  value={{this.data.final}}
  {{on "change" (pick "target.value" (fn this.update "final"))}}
>

Benefits here:

  1. The standalone update function can be tested trivially on its own, and it’s always just a function of the previous state and the change to apply.
  2. This correctly captures the fact that the state is a coherent bundle, which must be kept in sync, and it allows you to test that in a way that doesn’t even require rendering.
  3. If you ever need to change the logic for this (rounding, for example, or whatever else), there’s one and only one place to change it.
  4. The backing class actually gets to be much smaller!

This approach means you can export that update function locally and write unit tests against it and be confident that your logic will work, without worrying about the rest of the component behavior, including UI concerns.

For Case 2, I would do almost exactly the same same, with the same motivations and the same kinds of outcomes. You can have a blob of state which represents the frequency and counter values, and always update them together, with, again, a single action to invoke which knows how the two should relate.

Heuristics & Reasoning

As mentioned at the top, this recommendation flows out of a set of heuristics I use for thinking about how to design state (and reactivity):

  • Things which change together should live together. In OOP contexts, this is often used to describe how to think about what things should be in a class together. I apply that heuristic to state changes, too! This is one reason (though not the most important; see below) not to push state changes down into setters: now you’ve spread your changes to related state across three different setters, and nothing makes it obvious if you go to fix a bug or add a feature in one that you may also need to go do the same in the other two. Having a single function which handles it all together does make that obvious.

  • Not all related state is derived state. Sometimes, as in these examples, you have a set of state which is closely related, but where the actual root state is in fact the combination of all three of those values. In terms of thinking about whether you have derived state or related root state, the question is: which state is never set directly, but are simply functions of the other pieces of state? That’s derived state. (And there’s often more state which is just derived than Ember Classic led folks to realize, which can in turn lead us to overcorrect when there is a set of related root state.)

  • Each @tracked property should represent independent state. I often see folks try to have all of their reactive state live as top-level properties on a class (whether the class is a Component or just a standalone class to manage other tracked state). Sometimes that’s grand! Sometimes, as here, it actually doesn’t make sense, because these properties aren’t independent of each other. It doesn’t make any sense in this context to change base without changing final, etc. Here you want one @tracked property which represents the whole set of reactive state, because that set always has to stay in sync and change together. (This is the Octane/autotracking-specific application of the previous two heuristics.)

  • Setters are evil. I say that with a little bit of humorous exaggeration—but in all seriousness I have never found even a single case where a property setter which affects anything besides the setter itself is a good idea. An action (or more generally a method) much more obviously is going to do arbitrary things to state. For a setter to go change values that are, from the perspective of writing this.foo = 12, totally unrelated, substantially degrades your ability to understand the code locally. There’s no way to understand when reading this.foo = 12 that it also updates bar and baz (and even more so when it’s off in a separate context like a template!). You have to read the implementation of foo to know that. It’s a kind of “spooky action at a distance.”

2 Likes

Many thanks to both of you. I’m going to apply Chris’s philosophy for addressing this one, I think. It puts the whole relationship between the fields in one place, which is pretty compelling.

1 Like