Suggest an editImprove this articleRefine the answer for “Signals in Angular”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Angular Signals** are reactive state containers introduced in Angular 16. They track dependencies automatically and update only the UI parts that depend on the changed value, no Zone.js required. Use `signal()` for state, `computed()` for derived values, and `effect()` for side effects. ```typescript count = signal(0); double = computed(() => this.count() * 2); increment() { this.count.update(v => v + 1); } ``` **Key point:** only the bindings that read the changed signal re-render.Shown above the full answer for quick recall.Answer (EN)Image**Angular Signals** are reactive state containers introduced in Angular 16. They track dependencies automatically and update only the parts of the UI that actually read the changed value. ## Theory ### TL;DR - Think of Signals like a spreadsheet cell: change A1 and only the cells that reference A1 recalculate, not the whole sheet - Zone.js scanned the entire component tree after every event; Signals push updates only through a recorded dependency graph - Three primitives: `signal()` holds state, `computed()` derives values, `effect()` runs side effects - New app on Angular 16+: prefer Signals for local and shared state; keep RxJS for HTTP, WebSocket, and timer streams - Bridge between the two: `toSignal()` wraps an Observable, `toObservable()` does the reverse ### Quick example ```typescript import { Component, signal, computed } from '@angular/core'; @Component({ template: ` <p>Count: {{ count() }}</p> <p>Double: {{ double() }}</p> <button (click)="increment()">+</button> ` }) export class CounterComponent { count = signal(0); // writable signal double = computed(() => this.count() * 2); // recalculates only when count changes increment() { this.count.update(v => v + 1); // atomic update, not count.set(count() + 1) } } ``` Click "+" once: `count` becomes 1, `double` becomes 2. Only those two bindings re-render. Nothing else in the component tree is touched. ### Key difference from Zone.js Zone.js monkey-patches browser APIs and triggers a full component tree scan after every event, timeout, or HTTP response. Signals build a dependency graph at read time: when a template or `computed()` calls `count()`, Angular records that connection. On `set()` or `update()`, the scheduler traverses only the recorded edges and queues a microtask via `queueMicrotask`. No Zone.js needed. Angular benchmarks show 2-10x speed improvements on large component trees when running Signal-based apps in Zone-less mode. ### When to use - Local component state (form field, toggle, counter) → `signal()`, minimal setup - Shared app-wide state → Signal in an `@Injectable` service, injected where needed - Values derived from other state → `computed()`, cached and lazy - HTTP requests, WebSocket streams, timers → RxJS, then bridge with `toSignal()` for templates - Existing Zone.js codebase → Signals work alongside; migrate incrementally ### How it works internally `signal(0)` creates a node in a reactive graph. Calling `count()` inside `computed()` or a template binding records an edge from that reader to the signal node. On `count.set(1)`, the scheduler marks all dependent nodes dirty and queues a microtask. During the flush, Angular re-evaluates only the dirty nodes. `computed()` values are cached: if nothing upstream changed since the last read, the cached value is returned immediately without re-running the function. `effect()` follows the same pattern. It re-runs when any signal it read during the last execution changes. Before the next run, Angular calls the `onCleanup()` callback registered in the previous run, so you can cancel timers or abort fetch requests cleanly. In practice, teams often spend hours debugging Zone.js detection issues that disappear entirely once that piece of state moves into a Signal. ### Signals vs Observables | | Signals | Observables (RxJS) | |---|---|---| | Always has a value | Yes | No (need `BehaviorSubject`) | | Synchronous reads | Yes | Can be async | | Auto-cleanup | Yes | Manual `unsubscribe()` | | Template syntax | `{{ count() }}` | `{{ count$ \| async }}` | | Operators | None | Full RxJS operator set | | Designed for | Synchronous state | Async event streams | ### Common mistakes **1. Forgetting `()` on reads** ```typescript {{ count }} // shows the Signal object, no reactivity {{ count() }} // correct: registers as a dependency ``` **2. Reading a Signal in the constructor** ```typescript constructor() { console.log(this.count()); // executes once, never updates } // Fix: move reads into effect() or a lifecycle hook ``` **3. Using `set` with a manual read instead of `update`** ```typescript count.set(count() + 1); // reads twice, risks a stale value in concurrent code count.update(c => c + 1); // correct: single atomic transform ``` **4. Mutating Signal arrays directly** ```typescript items.value.push('new'); // no notification, array changed silently items.update(arr => [...arr, 'new']); // correct: new reference notifies dependents ``` **5. Stale closures in effects** ```typescript externalValue = 10; // Wrong: externalValue is captured once at creation, never re-tracked effect(() => console.log(this.count() * externalValue)); // Right: read the class property inside the effect body effect(() => console.log(this.count() * this.externalValue)); ``` ### Real-world usage - NgRx 17+ adopted Signals for store selectors, replacing RxJS `map` chains - Angular Material 16+ data tables accept a Signal array as `dataSource` - Angular 17 introduced `input.required<string>()` as a Signal-based replacement for `@Input()` - PrimeNG data tables use `toSignal()` to bridge HTTP Observables into templates - Nx workspaces use Signal-based services for cross-module state in micro-frontends ### Follow-up questions **Q:** What is the difference between a writable signal and a computed signal? **A:** A writable signal (`signal(0)`) exposes `set()` and `update()` methods to change its value. A `computed()` is read-only and derives its value from other signals automatically; calling `set()` on it throws an error. **Q:** How do Signals improve performance over Zone.js? **A:** Zone.js triggers a full tree scan after every async event. Signals update only dirty nodes in the dependency graph. Angular benchmarks record 2-10x speed gains on large apps in Zone-less mode. **Q:** When does a `computed()` recalculate? **A:** Only when at least one upstream signal has changed AND the computed value is actually read. If nothing reads it, recalculation is deferred. The result is cached between reads. **Q:** What does `effect()` do between runs? **A:** Before each re-run, Angular calls the `onCleanup()` callback from the previous run. Use it to cancel timers, abort requests, or remove event listeners. **Q:** In Angular 18+ Zone-less mode, how do Signals prevent UI tearing during batched updates? **A:** The scheduler batches multiple `set()` calls into one microtask flush. All `computed()` reads during that flush see a consistent snapshot of all signal values, so partial states never reach the template. ## Examples ### Basic counter with derived state ```typescript import { Component, signal, computed } from '@angular/core'; @Component({ selector: 'app-counter', template: ` <p>Count: {{ count() }}</p> <p>Status: {{ status() }}</p> <button (click)="increment()">+</button> <button (click)="reset()">Reset</button> ` }) export class CounterComponent { count = signal(0); status = computed(() => this.count() % 2 === 0 ? 'even' : 'odd'); increment() { this.count.update(c => c + 1); } reset() { this.count.set(0); } } ``` `status` re-evaluates only when `count` changes. Not on every event in the app, only on the exact dependency. ### Shared state in a service ```typescript // todo.service.ts import { Injectable, signal, computed } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class TodoService { private todos = signal<string[]>([]); count = computed(() => this.todos().length); completed = computed(() => this.todos().filter(t => t.startsWith('[x]')).length); addTodo(text: string) { this.todos.update(list => [...list, text]); } removeTodo(index: number) { this.todos.update(list => list.filter((_, i) => i !== index)); } } // In any component: // service = inject(TodoService); // template: {{ service.count() }} items, {{ service.completed() }} done ``` Any component that reads `service.count()` updates automatically when `addTodo` or `removeTodo` is called. No Subject, no `async` pipe, no manual unsubscribe. ### Input signals with computed validation (Angular 17+) ```typescript import { Component, input, computed } from '@angular/core'; @Component({ selector: 'app-user-card', template: ` <p>{{ name() }}</p> <span *ngIf="isAdult()">18+</span> <p>{{ roleLabel() }}</p> ` }) export class UserCardComponent { name = input.required<string>(); age = input(0); role = input<'admin' | 'user'>('user'); isAdult = computed(() => this.age() >= 18); roleLabel = computed(() => this.role() === 'admin' ? 'Administrator' : 'Member'); } ``` Parent template: `<app-user-card [name]="'Alice'" [age]="25" [role]="'admin'" />`. All three inputs are Signals under the hood, so `computed()` tracks them with no extra wiring and no `ngOnChanges`.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.