Signals are Angular's fine-grained reactive state primitive, and they let templates update only when the exact signal value changes.
Focus on reads, writes, computed signals, and the way signal tracking narrows the amount of work Angular has to do.
A strong understanding of signals should include the difference between reading a signal, updating it, and deriving state from it.
Angular Signals Reactive State signal should be studied as a practical Angular lesson, not as a label. Start by naming the input, the rule that changes the input, and the result a learner should be able to predict after reading the page.
In the angular > signals page, the notes should connect the definition with a working scenario, a mistake that beginners actually make, and the exact check that proves the fix. That makes the topic useful for coding, debugging, and interview revision.
A Signal is a wrapper around a value that notifies all consumers automatically whenever that value changes. Think of it as a reactive variable - when it updates, every part of the UI or logic that depends on it re-evaluates instantly.
Starting with Angular 16 (and made the default reactive primitive in Angular 21), Signals power Angular's new zoneless change detection. Instead of Zone.js patching every async operation and triggering full-tree checks, Angular now knows exactly which components need to re-render because Signals track their own dependencies.
Key characteristics of Signals:
Before Signals, Angular relied on Zone.js for change detection. While Zone.js worked, it came with real costs:
Signals solve all of these. They are explicit (Angular tracks exactly what changed), synchronous (no async pipe needed), and require zero extra libraries beyond Angular itself.
| Zone.js Problem | Impact |
|---|---|
| Ran change detection on the entire component tree | Slow in large apps |
| ~100 KB bundle overhead | Larger initial load |
| Monkey-patched browser APIs | Hard to debug, unexpected behaviour |
| Required RxJS BehaviorSubject for simple state | Boilerplate-heavy |
| async pipe needed in every template | Verbose templates |
signal(initialValue) creates a writable signal. You read it by calling it like a function - count(). You write to it with .set() for a direct value or .update() for a function that receives the current value.
The three operations you need to know:
| Operation | Syntax | When to use |
|---|---|---|
| Read | count() | In templates or computed/effect bodies |
| Set | count.set(5) | Replace with a known value |
| Update | count.update(v => v + 1) | Derive next value from current |
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1);
}
reset() {
this.count.set(0);
}
}
computed() creates a read-only signal whose value is derived from one or more other signals. It is lazy - it only recalculates when one of its signal dependencies actually changes, and it caches the result in between. You cannot call .set() or .update() on a computed signal.
Every time itemCount changes (via the button), total automatically recalculates. The template re-renders only the parts that read total() - nothing else.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart',
standalone: true,
template: `
<p>Items: {{ itemCount() }}</p>
<p>Price: ${{ price() }}</p>
<p><b>Total: ${{ total() }}</b></p>
<button (click)="addItem()">Add Item</button>
`
})
export class CartComponent {
itemCount = signal(1);
price = signal(9.99);
total = computed(() => this.itemCount() * this.price());
addItem() {
this.itemCount.update(n => n + 1);
}
}
effect() registers a callback that runs whenever any signal it reads changes. It is ideal for synchronising state to the outside world - writing to localStorage, updating the DOM directly, logging analytics, or syncing to a third-party library.
Important rule: do not modify signals inside an effect. Doing so can create infinite loops. Effects are for reading and reacting, not for writing back.
The effect runs once immediately on creation, then again every time theme() changes. Angular automatically cleans up the effect when the component is destroyed.
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-theme',
standalone: true,
template: `
<button (click)="toggleTheme()">
Theme: {{ theme() }}
</button>
`
})
export class ThemeComponent {
theme = signal('light');
constructor() {
effect(() => {
localStorage.setItem('theme', this.theme());
document.body.setAttribute('data-theme', this.theme());
});
}
toggleTheme() {
this.theme.update(t => t === 'light' ? 'dark' : 'light');
}
}
The most powerful pattern is placing signals inside an Injectable service. The service becomes the single source of truth for a piece of state. Any component that injects the service can read the signals directly in its template - no BehaviorSubject, no async pipe, no subscriptions to manage.
Expose the internal signal as .asReadonly() so consumers cannot accidentally mutate it. Only the service's own methods can change the state.
Multiple components can inject CounterService and they all share the same signal state. When one component calls svc.increment(), every other component reading svc.count() updates automatically.
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CounterService {
private _count = signal(0);
readonly count = this._count.asReadonly();
readonly doubled = computed(() => this._count() * 2);
readonly isZero = computed(() => this._count() === 0);
increment() { this._count.update(v => v + 1); }
decrement() { this._count.update(v => v - 1); }
reset() { this._count.set(0); }
}
import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<h2>Count: {{ svc.count() }}</h2>
<p>Doubled: {{ svc.doubled() }}</p>
<button (click)="svc.increment()">+</button>
<button (click)="svc.decrement()">-</button>
<button (click)="svc.reset()">Reset</button>
`
})
export class CounterComponent {
svc = inject(CounterService);
}
input() is the modern replacement for @Input(). The value is a read-only signal, so you can use it directly inside computed() and effect() without any extra wiring. Use input.required<T>() to mark an input as mandatory - Angular will throw a compile-time error if the parent does not provide it.
Because name and prefix are signals, message recomputes automatically whenever the parent changes either binding - no ngOnChanges needed.
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-greeting',
standalone: true,
template: `<h2>{{ message() }}</h2>`
})
export class GreetingComponent {
name = input.required<string>();
prefix = input('Hello');
message = computed(() => `${this.prefix()}, ${this.name()}!`);
}
// Usage: <app-greeting name="Angular" prefix="Welcome to" />
output() replaces @Output() + EventEmitter with a simpler, more explicit API. You call .emit(value) to fire the event, and the parent listens with the same (eventName)="handler($event)" syntax it always used.
import { Component, output } from '@angular/core';
@Component({
selector: 'app-like-button',
standalone: true,
template: `<button (click)="like()">Like ({{ likeCount }})</button>`
})
export class LikeButtonComponent {
likeCount = 0;
liked = output<number>();
like() {
this.likeCount++;
this.liked.emit(this.likeCount);
}
}
// Usage: <app-like-button (liked)="onLiked($event)" />
model() combines input() and output() into a single two-way bindable signal. It is the signal-based equivalent of [(ngModel)] for custom components. The parent binds with [(propertyName)] and the child can both read and write the value.
When the child calls this.checked.update(), Angular automatically emits a checkedChange event, which the two-way binding syntax [()] uses to update the parent's variable.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: `<button (click)="toggle()">{{ checked() ? 'ON' : 'OFF' }}</button>`
})
export class ToggleComponent {
checked = model(false);
toggle() {
this.checked.update(v => !v);
}
}
// Usage: <app-toggle [(checked)]="isActive" />
toSignal() from @angular/core/rxjs-interop bridges the RxJS world and the Signals world. It wraps an Observable and returns a Signal - no subscribe(), no async pipe, no manual unsubscribe(). Angular manages the subscription lifecycle automatically.
The signal starts as undefined until the HTTP response arrives, which is why the template checks @if (users()) before iterating. You can also pass { initialValue: [] } as a second argument to avoid the undefined state.
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-users',
standalone: true,
template: `
@if (users()) {
@for (user of users()!; track user.id) {
<div>{{ user.name }}</div>
}
} @else {
<p>Loading...</p>
}
`
})
export class UsersComponent {
private http = inject(HttpClient);
users = toSignal(
this.http.get('https://jsonplaceholder.typicode.com/users')
);
}
Signals and RxJS are complementary, not competing. Use the right tool for the job:
A practical rule of thumb: start with Signals for all synchronous state. Reach for RxJS when you need operators like debounceTime, switchMap, or retry, then bridge back to Signals with toSignal() for the template.
| Scenario | Signals | RxJS |
|---|---|---|
| Component state (counter, toggle, form field) | ✓ | |
| Derived / computed values | ✓ | |
| HTTP requests | via toSignal() | ✓ |
| WebSocket / real-time streams | ✓ | |
| Complex async pipelines (debounce, retry, switchMap) | ✓ | |
| Shared app state (user session, cart, theme) | ✓ | |
| Template binding without async pipe | ✓ |
| API | Import | Description |
|---|---|---|
| signal(value) | @angular/core | Creates a writable signal |
| computed(() => ...) | @angular/core | Derived read-only signal, lazy & cached |
| effect(() => ...) | @angular/core | Side effect that re-runs on signal change |
| input() | @angular/core | Signal-based @Input replacement |
| input.required() | @angular/core | Required signal input (compile-time enforced) |
| output() | @angular/core | Signal-based @Output replacement |
| model() | @angular/core | Two-way bindable signal (input + output) |
| viewChild() | @angular/core | Signal-based @ViewChild |
| contentChild() | @angular/core | Signal-based @ContentChild |
| toSignal(obs$) | @angular/core/rxjs-interop | Converts an Observable to a Signal |
| toObservable(sig) | @angular/core/rxjs-interop | Converts a Signal to an Observable |
const state = { topic: "Angular Signals Reactive State signal", ready: true };
if (state.ready) {
console.log(state.topic + ": render or run the normal path");
}
const response = null;
const message = response?.message ?? "Angular Signals Reactive State signal: show a clear fallback";
console.log(message);
Memorizing Angular Signals Reactive State signal without the situation where it is useful.
Connect Angular Signals Reactive State signal to a concrete Angular task.
Testing Angular Signals Reactive State signal only with the perfect input.
Include empty, missing, duplicate, incompatible, or failed cases when relevant.
Changing code before reading the visible symptom or error message.
Inspect the output, state, configuration, or stack trace connected to Angular Signals Reactive State signal.
Memorizing Angular Signals Reactive State signal without the situation where it is useful.
Connect Angular Signals Reactive State signal to a concrete Angular task.
Angular Signals are reactive primitives introduced in Angular 16. A signal holds a value and notifies consumers when it changes. They provide a simpler alternative to RxJS for state management.
<code>signal()</code> creates writable reactive state that you update manually with .set() or .update(). <code>computed()</code> creates read-only derived state that automatically recalculates when its signal dependencies change.
Use <code>effect()</code> for side effects like logging, syncing to localStorage, or calling external APIs when signal values change. Avoid updating signals inside effects to prevent infinite loops.
Yes. Use <code>toSignal(observable$)</code> to convert an Observable to a Signal, and <code>toObservable(mySignal)</code> to convert a Signal to an Observable. Both are in @angular/core/rxjs-interop.
Zoneless Angular removes Zone.js dependency and uses Signals for change detection. Enable it with <code>provideExperimentalZonelessChangeDetection()</code>. It significantly improves performance and reduces bundle size.
Explore 500+ free tutorials across 20+ languages and frameworks.