skip to content

As a lead, what rule would you set for when an Angular team may use effect(), and how would you enforce it?

level: principalimportance: should knowfreq 24%

answer

  1. last API you reach for
  2. outbound sync only
  3. derive, link, load, or handle
  4. justify every signal write
  5. make it visible in review

basics

~20 s

Allow effect() only to push signal state to non-signal APIs such as storage, logging, DOM or third-party libraries. Derived values use computed(), overridable ones linkedSignal(), async loads a resource, user intent an event handler; any signal write in an effect needs justification.

solid answer

~50 s

I'd adopt Angular's own position that effects are the last API to reach for, and write it as a decision list. Is the value a function of other signals? `computed()`. Derived but locally overridable? `linkedSignal()`. Loaded asynchronously from signal params? a resource API. Caused by a user action? the event handler. Only when state must flow **out** to something that is not a signal, like `localStorage`, analytics, a chart or imperative DOM, is `effect()` or `afterRenderEffect()` right. Signal writes inside effects are allowed since v19 but need a comment explaining why the value is not derivable and why it settles. Enforcement: a review checklist, a search or lint rule flagging `.set(`/`.update(` inside `effect(`, `debugName` on every effect so DevTools shows it, and tests that drive change detection. The tradeoff is some friction for new developers against far fewer loops, extra change detection passes and hidden data flows.

go deeper

for a junior

Recall that effects are for syncing to things outside signals, and that computed() is the default for derived values.

for a middle

Explain the decision list from computed() through linkedSignal(), resources and handlers to effect(), with one example of each.

for a senior

Show how you would review an effect: justify writes, check that they settle, add onCleanup and debugName, and test it by driving change detection.

for a principal

Own the policy: weigh strictness against delivery speed, decide where automation helps or adds noise, and set how the rule interacts with the team's RxJS guidance.

## Why a team needs a rule at all Angular 22.2 gives a team five ways to react to signal changes: `computed()`, `linkedSignal()`, resource APIs, `effect()` and `afterRenderEffect()`. Only the last two run arbitrary code on a schedule, and they are the easiest to reach for because they look like "when X changes, do Y". Unchecked, a codebase drifts into chains of effects that copy state between signals, which produces: - **extra change detection passes**, because a copied write can force already-checked views to be checked again; - **loops**, when an effect writes something that feeds its own inputs; - **hidden data flow**, since readers of a signal cannot see which effect writes it; - **timing bugs**, because effects run at scheduled points in change detection, not at the write. Angular's guide already takes a position: effects are "the last API you reach for", and "there are no situations where effect is good, only situations where it is appropriate". A lead's job is to turn that into something reviewable. ## The rule, as a decision list 1. Is the value a pure function of other signals? Use **`computed()`**. 2. Is it derived, but may the user override it locally until the source changes? Use **`linkedSignal()`**. 3. Does it load asynchronously from signal parameters? Use a **resource API**. 4. Does it happen because the user did something? Do it in the **event handler**. 5. Does signal state need to flow **out** to a non-signal API: storage, analytics, logging, imperative DOM, a canvas or chart library? Use **`effect()`**, or **`afterRenderEffect()`** if it needs the rendered DOM. Anything that does not reach step 5 is not an effect. ## Rules for the effects that remain | Rule | Reason | |---|---| | Create effects in constructors or field initializers, or pass `injector` | predictable lifetime and no `NG0203` | | Register `onCleanup` for timers, requests and listeners | no leaked work between runs | | A signal write inside an effect needs a comment justifying it | writes are allowed since v19, but most are disguised derivations | | Such writes must settle: the effect does not read what it writes, or writes a stable value | prevents loops | | Wrap incidental reads in `untracked()` | the dependency list matches intent | | Give each effect a `debugName` | visible in Angular DevTools | | `manualCleanup: true` only with an owner for `destroy()` | prevents effects outliving their feature | | Prefer explicit phases in `afterRenderEffect()` | the single-callback form is flagged as a performance risk | ## Enforcing it - **Review checklist.** Every new `effect(` gets the five questions above in the pull request. This is the cheapest control and catches most cases. - **Automated flagging.** A search or custom lint rule for `.set(` or `.update(` inside an `effect(` callback surfaces candidates; it will have false positives, so it should require a justification rather than block outright. - **Architecture placement.** Long-lived synchronisation, such as persisting a theme preference to `localStorage`, lives in one root service rather than being repeated per component, so there is one effect to reason about. - **Tests.** Effects run only when change detection runs, so tests drive it explicitly (`fixture.detectChanges()` or `TestBed.tick()`) and assert the outbound call, not the internal signal copies. - **Education.** Most propagation effects are written by developers arriving from frameworks where effects synchronise state. A short internal example of the refactor, from state-copying effect to `computed()`, pays for itself. ## Tradeoffs to acknowledge There is no single right answer, and a lead should state the costs: - **Strictness versus velocity.** A strict rule slows prototypes. Some teams allow propagation effects in spikes and require refactoring before merge. - **Legitimate writes exist.** Recording a measurement from an imperative API back into a signal, or bridging a non-signal source, may need a write. The rule should require justification, not forbid it. - **Signals versus RxJS.** Event streams with time semantics, such as debouncing, cancellation or retries, may be clearer in RxJS than in effects. Where that boundary sits is a separate team decision, and the effect rule should point to it rather than absorb it. - **Lint noise.** Over-eager automation trains people to suppress warnings. Keep automated checks narrow. ## A one-paragraph version for the team handbook "Use `effect()` only to push signal state out of Angular, to storage, logging, the DOM or a third-party library. Derive with `computed()`, derive-but-override with `linkedSignal()`, load with a resource, and react to user actions in handlers. Any signal write inside an effect needs a comment saying why it cannot be derived and why it cannot loop."

  • How would you roll this effect rule out on an existing Angular codebase full of state-copying effects?
    Inventory effects that call `set()` or `update()`, rank them by how often they cause bugs or extra passes, and refactor the worst to `computed()` or `linkedSignal()` first. Apply the rule strictly to new code only, and track the remaining count so it falls over time instead of blocking all work.
  • Would you ever allow effects that write signals in your Angular codebase?
    Yes, with justification: values that come from outside the signal graph, such as a measured size or a non-signal event source, may need a write. The comment must explain why the value cannot be derived and why the write settles, so it never re-triggers the effect indefinitely.

saying these in an interview costs you the question

  • Effects are fine for any state sync as long as they are small.
  • A lint rule can ban every signal write inside effects with no exceptions.
  • Since writes are allowed by default, effects are the intended way to sync signals.
  • The rule is only a performance concern, not a correctness one.
  • Moving every effect into root services removes the need for a rule.