Signals
|
This section documents modern, standalone Angular — signals, the built-in This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted. This section’s bibliography lists the reference material consulted while preparing these pages. |
A signal is a reactive value wrapper: reading it inside a reactive context (a template, a computed(), or
an effect()) records a dependency, so any consumer re-runs automatically when the value changes. Signals are
the default state primitive in modern Angular. See Signals.
signal()
signal(initialValue) returns a WritableSignal. Call it with no arguments to read; use .set(value) to
replace it and .update(fn) to derive the next value from the current one.
import { signal } from '@angular/core';
const count = signal(0);
count(); // read -> 0
count.set(10); // replace
count.update((n) => n + 1); // derive from current -> 11
<button (click)="count.set(count() + 1)">{{ count() }}</button>
By default a signal uses Object.is to decide whether the value changed; supply an equality function to
treat structurally equal values as unchanged and skip notifying consumers.
import _ from 'lodash-es';
const point = signal({ x: 0, y: 0 }, { equal: _.isEqual });
point.set({ x: 0, y: 0 }); // deep-equal -> consumers do NOT re-run
computed()
computed(fn) creates a read-only signal derived from other signals. It is lazy (the body runs only when
read), memoised (re-runs only when a tracked dependency changes), and glitch-free (never exposes a
partially-updated intermediate value).
import { signal, computed } from '@angular/core';
const price = signal(100);
const qty = signal(3);
const total = computed(() => price() * qty()); // tracks price and qty
total(); // 300
qty.set(4);
total(); // 400 -- recomputed on read
effect()
effect(fn) runs a side effect whenever any signal it reads changes. It runs once after creation, then again
(asynchronously, batched, after change detection) on each dependency change. Register it in an injection
context; it is cleaned up automatically on destroy. Return a cleanup callback via the onCleanup argument to
tear down the previous run.
import { Component, effect, signal, inject } from '@angular/core';
import { Analytics } from './analytics';
@Component({ selector: 'app-search', templateUrl: './search.html' })
export class Search {
readonly query = signal('');
private readonly analytics = inject(Analytics);
constructor() {
effect((onCleanup) => {
const q = this.query();
const handle = setTimeout(() => this.analytics.track('search', q), 300);
onCleanup(() => clearTimeout(handle)); // cancel if query changes again
});
}
}
Use untracked(fn) to read a signal without creating a dependency — so the effect reacts to some signals
but merely samples others.
import { untracked } from '@angular/core';
effect(() => {
const q = this.query(); // reactive: re-runs when query changes
const user = untracked(() => this.userId()); // sampled: changes here do NOT re-run
this.log(q, user);
});
Prefer computed() for deriving state and reserve effect() for genuine side effects (logging, DOM APIs,
syncing to localStorage); do not set() other signals from an effect. See
Effects and the note on
reading without tracking.
linkedSignal() and resource()
linkedSignal() produces a writable signal that also derives from a source: it recomputes when the source
changes but can be overridden locally until the next source change — ideal for a "selected item" that resets
when the list reloads.
import { signal, linkedSignal } from '@angular/core';
const options = signal<string[]>(['sm', 'md', 'lg']);
const choice = linkedSignal(() => options()[0]); // follows options, but writable
choice.set('lg'); // local override
options.set(['xs', 's']);
choice(); // 'xs' -- reset to the new default
resource() wraps an async data source in signals, exposing .value(), .status(), .error(), and
.reload(), and cancels an in-flight request when its params change. rxResource() is the same backed by
an Observable. HTTP-specific fetching (httpResource()) is covered on
HTTP client.
import { resource, signal } from '@angular/core';
const userId = signal(1);
const user = resource({
params: () => ({ id: userId() }),
loader: ({ params, abortSignal }) =>
fetch(`/api/users/${params.id}`, { signal: abortSignal }).then((r) => r.json()),
});
user.status(); // 'idle' | 'loading' | 'resolved' | 'error' | 'reloading'
References: linkedSignal and
resource.
Signals vs. RxJS
Both are reactive; they solve different problems.
| Reach for a signal | Reach for RxJS (see RxJS and async) |
|---|---|
Synchronous UI state that always has a current value |
Streams of events over time (keystrokes, websocket messages) |
Derived values ( |
Time-based combinators: |
Template bindings, component state |
Cancellation and switching between requests ( |
Inputs, model, and queries (all signal-based) |
Complex multi-source orchestration and back-pressure |
Bridge between them with toSignal(obs$) and toObservable(sig) from @angular/core/rxjs-interop. Signal
inputs (input()), model (model()), and queries (viewChild() / contentChildren()) are all
covered on Component communication. See
RxJS interop.