Skip to the solution
HardFrontend12 min read

What is the effect() function in Angular Signals, and when should you NOT use it?

103 views

The problem

Question presented to candidate: "Our codebase has about forty effect() calls and a few of them keep causing stale values and one froze a page. Explain what effect() actually does in Angular, when it runs, and which of those forty you would delete."

What a strong answer should cover:

  • effect() registers a function that re-runs whenever a signal it read last time changes, and exists for side effects: syncing signal state out to something that is not a signal (DOM APIs, storage, logging, a third-party widget).
  • Timing: effects are scheduled, not synchronous. Several set() calls in one handler produce one run with the final value. Component effects run during change detection before that component's template refreshes; root effects (created outside the component tree) run first, as part of the app tick.
  • It needs an injection context (or an injector option) and is destroyed with its owner; onCleanup cancels work from the previous run.
  • When NOT to use it: deriving state (use computed()), resettable derived state (use linkedSignal()), copying one signal into another, loading data (use resource()/httpResource()/rxResource()), DOM reads after render (use afterRenderEffect() / afterNextRender()).
  • An effect that writes a signal it also reads can loop forever; Angular does not stop it.
  • untracked() to read without subscribing; allowSignalWrites is deprecated because writes are now allowed.

Clarifying questions expected:

  • "Are the stale values coming from effects that copy one signal into another?"
  • "Are these effects in components or in root services? Their timing differs."

Code / implementation expected: Yes β€” one legitimate effect with cleanup, and the same derived value written as an effect versus computed()/linkedSignal().

  • signals
  • reactivity

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read Β· 10 sections

Read the code

AngularLegitimate effects: syncing document.title and cancelling stale work with onCleanup
import { Component, computed, effect, signal } from "@angular/core";

// Good uses of effect(): pushing signal state OUT to something that is not a signal
// (document.title, localStorage, a log, a third-party widget).
@Component({
  selector: "app-root",
  template: `
    <h3>Reading preferences</h3>
    <button (click)="dark.update(d => !d)">Theme: {{ dark() ? "dark" : "light" }}</button>
    <button (click)="size.update(s => s + 2)">Bigger text</button>
    <p>{{ summary() }}</p>
  `,
})
export class AppComponent {
  dark = signal(false);
  size = signal(14);

  // Derived state is computed(), never an effect that writes another signal.
  summary = computed(() => `${this.dark() ? "Dark" : "Light"} theme, ${this.size()}px`);

  effectRuns = 0;

  constructor() {
    // Side effect 1: sync to things outside Angular. Re-runs when dark or size change.
    effect(() => {
      this.effectRuns++;
      document.title = this.summary();
      console.log(`save prefs: dark=${this.dark()} size=${this.size()}`); // e.g. localStorage.setItem(...)
    });

    // Side effect 2: work that must be cancelled when inputs change again.
    effect((onCleanup) => {
      const size = this.size();
      const timer = setTimeout(() => console.log(`analytics: settled on ${size}px`), 50);
      onCleanup(() => clearTimeout(timer)); // runs before the next run, and on destroy
    });
  }
}
Angularcomputed() and linkedSignal() versus copying state with an effect
import { Component, computed, effect, linkedSignal, signal } from "@angular/core";

const METHODS: Record<string, string[]> = {
  UK: ["Royal Mail", "DPD"],
  US: ["USPS", "UPS", "FedEx"],
};

@Component({
  selector: "app-root",
  template: `
    <button (click)="switchCountry()">Ship to {{ country() === "UK" ? "US" : "UK" }}</button>
    <p>computed total: {{ totalComputed() }} | effect copy: {{ totalCopy() }}</p>
    <p>linkedSignal method: {{ method() }} | effect-reset method: {{ methodViaEffect() }}</p>
    <p>read inside the click handler: {{ readInHandler() }}</p>
    <button (click)="method.set(methods()[1])">Pick 2nd method</button>
  `,
})
export class AppComponent {
  country = signal("UK");
  qty = signal(2);
  methods = computed(() => METHODS[this.country()]);

  // DO: derived values are computed() ...
  totalComputed = computed(() => this.qty() * (this.country() === "UK" ? 5 : 7));
  // ... and "derived but user-overridable" state is linkedSignal().
  method = linkedSignal(() => this.methods()[0]);

  // DON'T: copy state into another signal from an effect.
  totalCopy = signal(0);
  methodViaEffect = signal("");
  readInHandler = signal("");

  constructor() {
    effect(() => this.totalCopy.set(this.qty() * (this.country() === "UK" ? 5 : 7)));
    effect(() => this.methodViaEffect.set(this.methods()[0]));
  }

  switchCountry() {
    this.country.update((c) => (c === "UK" ? "US" : "UK"));
    // Same tick, straight after the write: the computed values are already right,
    // the effect copies have not run yet.
    this.readInHandler.set(
      `computed=${this.totalComputed()} copy=${this.totalCopy()} linked=${this.method()} effectReset=${this.methodViaEffect()}`,
    );
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 119 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

Explain how you can bridge Signals and RxJS observables using toSignal() and toObservable().

Continue