Skip to content
Angular ng signals 4 min read

Introduction to Signals

Signals are Angular’s reactive primitive: a wrapper around a value that knows when it is read and notifies anything that depends on it when the value changes. Introduced as stable in Angular 16+ and expanded heavily through 17, 18, and 19, signals give the framework fine-grained reactivity, meaning Angular can update exactly the parts of the DOM that depend on a changed value instead of re-checking an entire component tree. If you have ever wrestled with ChangeDetectionStrategy, manual markForCheck() calls, or runaway re-renders, signals are the modern answer.

What is a signal?

A signal is a function that returns the current value when you call it. Calling the signal (count()) reads the value and, when done inside a reactive context such as a template or a computed, registers a dependency. Updating the signal (count.set(5)) changes the value and schedules every dependent computation and view for an update.

import { signal } from '@angular/core';

const count = signal(0);

console.log(count());   // read the current value
count.set(5);           // replace the value
count.update(n => n + 1); // derive the next value from the current one
console.log(count());

Output:

0
6

Because reading a signal is just a function call, there is no special syntax to subscribe and nothing to unsubscribe from. The dependency graph is tracked automatically and torn down when a consumer is destroyed.

Why signals matter

Traditional Angular relied on Zone.js to detect that something changed and then dirty-checked components. Signals flip this around: a value declares precisely who depends on it, so updates are targeted and predictable. This unlocks zoneless change detection (provideZonelessChangeDetection()), better performance on large views, and a mental model that is easy to reason about.

AspectZone.js / default CDSignals
Update granularityWhole component treeOnly dependent expressions
SubscriptionImplicit, via zonesTracked automatically on read
BoilerplatemarkForCheck, async pipeRead the signal directly
Zoneless supportNoYes
DebuggabilityHard to traceExplicit dependency graph

Using a signal in a component

Signals shine inside standalone components. Read the signal directly in the template, and Angular keeps that binding in sync without an async pipe or manual change detection.

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    @if (count() > 0) {
      <p>Positive!</p>
    }
    <button (click)="increment()">Add</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class CounterComponent {
  protected readonly count = signal(0);

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

  reset(): void {
    this.count.set(0);
  }
}

Notice the new @if control flow reading count() directly. Each time count changes, only the text interpolation and the @if block are re-evaluated, not unrelated parts of the page.

Reading vs. writing

A WritableSignal (the return type of signal()) exposes three mutating methods. Read-only signals (such as those returned by computed() or input()) expose none of them.

OperationMethodUse when
Readvalue()You need the current value (registers a dependency in reactive contexts)
Setvalue.set(x)You have the next value outright
Updatevalue.update(fn)The next value derives from the current one
Read-only viewvalue.asReadonly()You want to expose a signal without write access

Tip: Treat signal values as immutable. Always produce a new object or array with set/update rather than mutating in place — Angular compares by reference, so an in-place mutation will not notify consumers.

const todos = signal<string[]>([]);

// Correct: new array reference
todos.update(list => [...list, 'Write docs']);

// Wrong: mutating in place does NOT trigger updates
// todos().push('Broken');

Equality checks

By default signals use Object.is to decide whether a value actually changed; setting a signal to a value equal to the current one is a no-op and skips notifications. You can supply a custom equal function for value objects.

import { signal } from '@angular/core';

const point = signal(
  { x: 0, y: 0 },
  { equal: (a, b) => a.x === b.x && a.y === b.y }
);

point.set({ x: 0, y: 0 }); // no change detected, consumers not notified

Best practices

  • Read signals only where you need them; reading inside a computed or template is what builds the dependency graph.
  • Keep signal values immutable — return new objects/arrays from update instead of mutating.
  • Expose write-protected state with asReadonly() so consumers cannot bypass your component’s API.
  • Prefer update(fn) over set when the next value depends on the current one to avoid stale reads.
  • Derive state with computed() rather than duplicating it in multiple writable signals.
  • Adopt zoneless change detection on new apps to get the full performance benefit of fine-grained reactivity.
Last updated June 14, 2026
Was this helpful?