Lesson 5 of 30
signal(), computed(), and effect() are the reactive primitive underneath everything modern Angular does — no subscribe, no operators, just a value you call as a function.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A signal is a box holding a value that Angular knows how to watch. You read it by calling it like a function — count() — and you change it with .set() or .update(). That's the entire API surface for the basic case. What makes signals matter isn't the box itself, it's that Angular's rendering system, computed(), and effect() all automatically track which signals were read while they ran — so when one of those signals changes, only the things that actually depend on it re-run. No subscribe/unsubscribe, no dependency arrays to keep in sync by hand, no observable operators. You read a signal, and the read itself is what registers the dependency.
Three primitives cover almost everything:
signal(initialValue) — a writable, reactive value.computed(() => ...) — a derived, read-only value that recalculates only when a signal it reads has changed, and only when something actually asks for it again (lazy and memoized — cheap to declare many of these).effect(() => ...) — a side effect that re-runs automatically whenever a signal it reads changes. Use it for things that reach outside the reactive graph (logging, syncing to localStorage, imperative DOM work) — not for deriving one signal from another; that's what computed() is for.import { signal, computed, effect } from '@angular/core';
const count = signal(0);
const doubled = computed(() => count() * 2);
const isEven = computed(() => count() % 2 === 0);
effect(() => {
console.log(`count=${count()} doubled=${doubled()} isEven=${isEven()}`);
});
count.set(5);
// effect logs: count=5 doubled=10 isEven=false
count.update((current) => current + 1);
// effect logs: count=6 doubled=12 isEven=true
Nobody told doubled or isEven to depend on count — they discovered that dependency the first time they ran, because they called count() while executing. Same for the effect: it reads count(), doubled(), and isEven(), so Angular knows to re-run it whenever any of the three changes — even though only count was ever written to directly. That propagation, one write fanning out through every computed and effect that transitively reads it, is the entire mental model. In a component template, reading a signal works the exact same way — the template itself is treated as a reactive consumer, which is what lets Angular skip re-rendering parts of the DOM that don't depend on whatever changed.
A signal holds a value directly — count = signal(0). Reading count() returns 0, and Angular tracks who read it.
.subscribe(), no operators like map/filter/debounceTime, no teardown to manage. You read a signal by calling it. If you need Observable-style stream operators, that's what the RxJS-interop bridge (toSignal/toObservable) is for — signals themselves are a much smaller, synchronous primitive.effect() to set another signal. Writing effect(() => otherSignal.set(count() * 2)) works, but it builds a hidden, harder-to-trace dependency chain and can trigger extra change-detection cycles. If a value is derived from another signal, reach for computed() — that's exactly the case it exists for.count() inside a plain, non-reactive function (say, a one-off console.log in an event handler) works fine and returns the current value, but nothing gets registered as a dependency, because there's no computed/effect/template currently "listening." That's not a bug — it's just not automatically reactive there.If you've used React, signal() is closest to a useState cell, and computed() plays the role useMemo plays — except a computed() never needs a dependency array, because Angular already knows exactly which signals it read by watching the function execute, rather than trusting a list you maintain by hand. The next lesson, on the new @if/@for control-flow syntax, is where you'll see signals actually driving what gets rendered in a template.