vitaly-t
vitaly-t

Reputation: 25900

Angular effects and conditional use of signals

A concept that illudes me about Angular signals - conditional use of signals inside effects:

effect(() => {
    const count = this.outsideFlag ? this.total() : this.current();
    console.log(`The count is: ${count}`);
});

Above we make conditional use of two signals - total + current. And because it is conditional, Angular is going to fail to detect a later change on both signals (will do only for one that executed on the first run).

Does this not severely undermine the whole concept of signals and effects? And how are we supposed to counter such a shortcoming in the change detection?

UPDATE

Consider effect calling a class method, which in turn makes conditional use of signals. This also won't work, but worse - you cannot design class methods based on whether or nor they will be invoked from within effect. This makes implementation inside effects very error-prone. And you cannot address this issue through automation tests either.

Upvotes: 4

Views: 15608

Answers (4)

Christian Watt
Christian Watt

Reputation: 11

I have actually ran into the opposite, where to many signals are triggering the effect. As a common practice I have gone to this:

effect(() => {
    let c = this.current();
    let t = this.total();
    untracked(current = this.outsideFlag ? t: c;
       //especially if you are calling any other functions that may have signals in them that you didn't know existed that may cuase this effect to fire when you didn't think it should!
    )
});

So above I use to let statements, or as many as you need to control if this changes, fire this effect. I put everything else in an untracked so even if there is a signal down the road that may be called in following logic, it will not trigger this effect because by being wrapped in the untracked this effect does not register to their changes. I pretty much write every effect logic this way. I do wish you could define what signals to watch, but this is pretty basic to implement as a consistent pattern and save headaches down the road!

Upvotes: 1

Francisco Santorelli
Francisco Santorelli

Reputation: 1338

wouldn't this work though?

effect(() => {
    const total = this.total();
    const current = this.current();
    const count = this.outsideFlag ? total : current;
    console.log(`The count is: ${count}`);
});

Upvotes: 2

Simon_Weaver
Simon_Weaver

Reputation: 146160

Take it to the other extreme. NO signals in your effect. What mechanism could possibly detect a change in ANY of the properties used? Could you ever expect it to? If you do you just invented regular Angular change detection! You just can’t have it both ways.

With your example:

this.outsideFlag ? this.total() : this.current();

Even if you could provide dependencies (total and current) to the effect it wouldn’t even help.

Consider this sequence

  • outsideFlag starts as true
  • so value of total is returned
  • value of total is changed outside
  • effect runs again
  • current is set to a new value
  • no change occurs or should occur
  • flag is changed to false
  • angular has no way to detect it
  • the fact we were ‘watching’ current didn’t help at all.

You’re either using signals or you’re not. And yes this kind of issue can lead to all kinds of ‘bugs’ or things that don’t get updated. So when you find the bug you have to make it a signal. There’s no other way to get the performance improvements promised by signals.

At the end of the day it’s the responsibility of the developer to understand the toolset.

Upvotes: 2

André C. Andersen
André C. Andersen

Reputation: 9405

effect() and compute() are in principle pretty similar. The difference is that effect() doesn't return a signal object. I mention this because the signal documentation addresses conditionals in context of compute(). This is their example:

const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
  if (showCount()) {
    return `The count is ${count()}.`;
  } else {
    return 'Nothing to see here!';
  }
});

When reading conditionalCount, if showCount is false the "Nothing to see here!" message is returned without reading the count signal. This means that updates to count will not result in a recomputation.

If showCount is later set to true and conditionalCount is read again, the derivation will re-execute and take the branch where showCount is true, returning the message which shows the value of count. Changes to count will then invalidate conditionalCount's cached value.

Note that dependencies can be removed as well as added. If showCount is later set to false again, then count will no longer be considered a dependency of conditionalCount.

Their "solution" is, as @Andrei in the comments explain, to make the conditional a signal too:

effect(() => {
    const count = this.outsideFlag() ? this.total() : this.current();
    console.log(`The count is: ${count}`);
});

A change in outsideFlag() will make the effect recompute, invoking the other signal. This is a feature-not-a-bug, as it saves computation. If it wasn't like this, you would have to recompute all branches for all changes. (Kind of like @Eldar in the comments suggestion.)

As for cases where the conditional is hidden:

Consider the case when effect invokes a class method, which in turn uses signals.

The conditionals in those class methods should also be signals.

You should use signals all-the-way-down.

Upvotes: 9

Related Questions