codejump

AcademyAngular

Signals: signal(), computed() and effect()

signal() holds a value behind a getter/setter pair; computed() derives from it lazily; effect() reacts to it. Angular's answer to Vue's ref or a React hook.

2 min readUpdated

3 exercises

Waiting at the end of this page.

signal() is called to read, and called with an argument to write

signal(initialValue) returns a function-shaped object: calling it with no arguments reads the current value, and .set()/.update() write a new one:

typescript
count = signal(0);

increment(): void {
  this.count.update((n) => n + 1);
}

The bug shape this invites is subtle: this.count.update((n) => n++) looks like it increments, but n++ returns the old value of n as the expression's result — post-increment evaluates to the pre-increment value — so the updater function returns the unchanged number back into .update(), and repeated clicks visibly do nothing, or produce NaN once that stale value feeds into something else expecting a number. (n) => n + 1 is the version that actually returns the new value.

computed() is lazy and re-derives only from the signals it reads

computed(() => expr) returns a read-only signal that recomputes only when a signal it actually depends on changes — not on a timer, not on every change-detection pass:

typescript
count = signal(0);
doubled = computed(() => this.count() * 2);

If doubled looks like it "never updates," the usual cause is that the template reads a plain field instead of calling the computed signal — {{ doubled }} instead of {{ doubled() }} — which prints the function reference's string form instead of its result, since a signal is a callable, not a plain value.

effect() runs after creation, not synchronously in the constructor

effect(() => { ... }), registered in a constructor or field initializer, runs once shortly after creation and again whenever a signal it reads changes — but not synchronously the instant you write it, and never before the component's fields have actually been assigned. An effect that mutates a signal it also reads, in place, deadlocks conceptually the same way a Vue watcher mutating its own source does: reassign a whole new array or object from inside the effect, do not .push() onto the one you just read.

What interviewers check

Whether you know that signal() is read by calling it, that computed() is lazy rather than eager, and whether you have hit the "n++ doesn't increment a signal" bug yourself — it is the fastest way to tell someone who has written Signals code from someone reciting the API.

Now practice it

1 quiz · 2 drills

Reading this page is the cheap half. These are the exercises that make you use it.