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:
@angular/core.Before Signals, Angular relied on Zone.js for change detection. While Zone.js worked, it came with real costs:
| 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 |
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.
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.
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);
}
}
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 |
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.
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);
}
}
Every time itemCount changes (via the button), total automatically recalculates. The template re-renders only the parts that read total() - nothing else.
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.
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 effect runs once immediately on creation, then again every time theme() changes. Angular automatically cleans up the effect when the component is destroyed.
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.
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);
}
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.
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.
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" />
Because name and prefix are signals, message recomputes automatically whenever the parent changes either binding - no ngOnChanges needed.
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.
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" />
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.
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.
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')
);
}
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.
Signals and RxJS are complementary, not competing. Use the right tool for the job:
| 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 | ✓ |
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.
| 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 |
signal() creates a reactive state container.
computed() creates a derived signal that automatically updates when its dependencies change.
effect() runs a side effect whenever any signal it reads changes.
input() and output() for signal-based component communication (Angular 17+).
model() creates a two-way bindable signal - replaces [(ngModel)] in standalone components.
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.
signal() creates writable reactive state that you update manually with .set() or .update(). computed() creates read-only derived state that automatically recalculates when its signal dependencies change.
Use effect() 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 toSignal(observable$) to convert an Observable to a Signal, and toObservable(mySignal) 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 provideExperimentalZonelessChangeDetection(). It significantly improves performance and reduces bundle size.
Explore 500+ free tutorials across 20+ languages and frameworks.