0

Why do these two functions #workingUpdate and #brokenUpdate behave differently? I actually would expect both functions to fail since the ElementRef does not exist while the constructor is called.

@Component({
  selector: 'test-app',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
  <button (click)="addOne()">count+</button><br/>
  count: {{count()}}<br/>
  output1: <span #output1></span><br/>
  output2: <span #output2></span><br/>
  `,
})
export class App {
  count = signal(0);

  @ViewChild('output1') output1!: ElementRef;
  @ViewChild('output2') output2!: ElementRef;

  #workingUpdate = () => {
    const c = this.count();
    this.output1.nativeElement.innerHTML = c;
  };
  #brokenUpdate = () => {
    this.output2.nativeElement.innerHTML = this.count();
  };

  constructor() {
    effect(this.#workingUpdate);
    effect(this.#brokenUpdate);
  }

  addOne() {
    this.count.update((c) => c + 1);
  }
}

I created a simple Example. I originaly ran into this problem calling a update interface of a third party component. I use the #workingUpdate stucture to fix my problem, but now im not sure if i've just hidden the actual problem.

Chellappan வ
  • 23,645
  • 3
  • 29
  • 60
Thomas
  • 3
  • 1

1 Answers1

1

To answer the specific question, from the docs:

An effect is an operation that runs whenever one or more signal values change.

Effects always run at least once. When an effect runs, it tracks any signal value reads. Whenever any of these signal values change, the effect runs again. Similar to computed signals, effects keep track of their dependencies dynamically, and only track signals which were read in the most recent execution.

Effects always execute asynchronously, during the change detection process.

Your example is very interesting.

It does work if I add console.log() and read the signal there.

  #workingUpdate = () => {
    console.log('working', this.count());
    const c = this.count();
    this.output1.nativeElement.innerHTML = c;
  };
  #brokenUpdate = () => {
    console.log('broken', this.count());
    this.output2.nativeElement.innerHTML = this.count();
  };

Almost looks like it isn't correctly registering the signal for the effect in the #brokenUpdate case?

UPDATE: I got an answer on this from the Angular team.

Basically, the brokenUpdate case doesn't ever register the signal since it throws an exception the first time. So it is never getting re-run when the signal changes.

The workingUpdate case registers the signal before throwing the error. So it is re-run when the signal changes.

One way to fix it is to make the element refs static so they are there when the effects run. Something like this:

  @ViewChild('output1', { static: true }) output1!: ElementRef;
  @ViewChild('output2', { static: true }) output2!: ElementRef;
DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • I updated the answer to include information I received from the Angular team. – DeborahK Jul 08 '23 at 17:42
  • Thank you for your answer. I don't think the behaviour is very intuitive, because in the end both functions should invoke the same error of output(1|2) not being defined. But that might be just a "me problem" :). – Thomas Jul 10 '23 at 08:12
  • If you comment out the `brokenUpdate` effect and just run the `workingUpdate`, you'll see that it *does* generate an error the first time. But unlike an Observable, an effect will re-execute after an error if its signal is registered before the error occurs. So the `workingUpdate` generates the error the first time. Then when you click the count button the signal is changed and the effect runs again. But by then the `output2` is defined and it works from that point on. – DeborahK Jul 10 '23 at 14:47
  • 1
    Ah okay, i think i understand it now. Thank you again. I need to dive a bit deeper into signals and error handling. – Thomas Jul 10 '23 at 18:31