← All articles

Angular Signal Effects Are Clearer with IIFEs

Why effects skip async and conditional signal reads, and two patterns—IIFE and first-line assignment—to register dependencies reliably.

Published
Angular Signal Effects Are Clearer with IIFEs cover image

Angular Signal effects are handy: they run whenever Signals used inside the effect change.

effect(() => {
  console.log('signal variable is changed to ' + signalVariable());
})

That works when the Signal is read on the first pass, but reads inside async code or conditionals are skipped.

effect(() => {
  // Runs whenever this.count changes
  this.insideSimpleEffect = this.count();
});
effect(async () => {
  // Does not run
  await new Promise<void>((resolve) => resolve());
  this.insideAwaitRequestAnimationFrame = this.count();
});
effect(() => {
  // Does not run
  requestAnimationFrame(() => {
    this.insideRequestAnimationFrame = this.count();
  });
});
effect(() => {
  // Does not run
  setTimeout(() => {
    this.insideSetTimeout = this.count();
  }, 100);
});
effect(() => {
  // Does not run
  if (this.insideSimpleEffect === 5) {
    this.insideCondition = this.count();
  }
});

You can confirm they do not run at https://stackblitz.com/edit/stackblitz-starters-unvngn?file=src%2Fmain.ts.

Cause

Effects register dependencies only when a Signal is actually read. If a read is hidden in a conditional or happens on another stack frame in async work, the effect cannot register the dependency, so it will not react when the Signal changes.

Source: https://github.com/angular/angular/issues/56773#issuecomment-2198479276

Mitigation

1. Assign on the first line always

To register dependencies, assign from every Signal you use on the first line. Simplest approach, but easy to miss because it depends on the author’s attention.

ts
  effect(() => {
+   const count = this.count();
    if (this.insideSimpleEffect === 5) {
-     this.insideCondition = this.count();
+     this.insideCondition = count;
    }
  });

2. Use an IIFE

In an effect, call an IIFE and pass Signals as arguments to register dependencies. I prefer this—it makes the effect’s purpose clearer.

effect(() => ((count: number) => {
  if (this.insideSimpleEffect === 5) {
    this.insideCondition = count;
  }
})(this.count()));

Summary

Signals are very ergonomic, especially for form change detection. Without understanding this behavior, they become a source of bugs, so I recommend codifying rules for how you use them.

See you next time.