Angular introduced a major feature in the v16-next release โ€” Signals. This feature represents a huge leap for the framework and could, in my opinion, significantly boost Angular's popularity in the coming years. Why? Because it finally addresses a long-standing limitation: Zone.js.

Let's dive into the backstory behind this shift and explore what makes Signals such a game-changer.

๐Ÿ”ง Understanding Angular's Current Change Detection: Zone.js

To appreciate why Signals are a big deal, we need to understand how Angular has traditionally handled reactivity โ€” via Zone.js.

What is Zone.js?

Zone.js is Angular's default mechanism for detecting changes in your app. Whenever an event (like a click or a timer) occurs, it's the browser โ€” not Angular โ€” that triggers it. Zone.js steps in by monkey-patching standard JavaScript APIs (like window, document, and even setTimeout) to intercept these browser events. Once intercepted, Angular kicks off a change detection cycle to update the DOM.

Why is Zone.js problematic?

While Zone.js has been reliable and powerful, it comes with several limitations:

  • It only knows something might have changed, not what exactly changed or where.
  • Because of this uncertainty, Angular often rechecks the entire component tree โ€” which can be overkill for large applications.
  • Debugging is harder since monkey-patching masks native behavior.
  • Native JavaScript features like async/await can't be monkey-patched, so workarounds are needed.
  • Performance becomes an issue in large-scale apps as the app grows and change detection becomes more expensive.

Some existing strategies like OnPush change detection and pure pipes help, but they don't solve the root problem.

๐Ÿ”ฎ Enter Signals: Angular's Future of Reactivity

This is where Signals come into play โ€” a modern reactivity model designed to give Angular fine-grained control over updates.

What's a Signal?

A Signal is essentially a reactive wrapper around a value. When the value changes, it notifies only those who care โ€” the consumers โ€” rather than rechecking everything.

There are two main types:

  • Writable Signals โ€” You update them directly via a mutation API.
  • Computed Signals โ€” Their value is derived from other signals and updates automatically when dependencies change.

โœ… Writable Signals (Direct Mutation)

A writable signal holds a value that can be directly updated using the set() or update() methods. It's similar to using a BehaviorSubject in RxJS but with Angular's reactivity baked in.

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

@Component({
  selector: 'counter-component',
  standalone: true,
  template: `
    <h2>Counter: {{ count() }}</h2>
    <button (click)="increment()">Increment</button>
  `,
})
export class CounterComponent {
  // Writable signal
  count = signal(0);

  increment() {
    this.count.update((current) => current + 1);
  }
}

๐Ÿ” What's happening?

  • count is a signal holding a number.
  • The template uses count() to read the signal.
  • When the button is clicked, update() changes the value, and only the parts of the UI that read count() are updated.

๐Ÿ” Computed Signals (Derived Values)

A computed signal automatically recalculates its value whenever any signal it depends on changes. You don't mutate it directly โ€” it's derived. Let's look at a simple example that the Angular team shared โ€” converting Celsius to Fahrenheit:

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

@Component({
  selector: 'temperature-converter',
  standalone: true,
  template: `
    <label>
      Celsius:
      <input type="number" [value]="celsius()" (input)="onInput($event)" />
    </label>
    <p>Fahrenheit: {{ fahrenheit() }}</p>
  `,
})
export class TemperatureConverterComponent {
  // Writable signal
  celsius = signal(0);

  // Computed signal
  fahrenheit = computed(() => this.celsius() * 1.8 + 32);

  onInput(event: Event) {
    const value = +(event.target as HTMLInputElement).value;
    this.celsius.set(value);
  }
}

Notice how the Fahrenheit value automatically recalculates when Celsius changes โ€” with no manual wiring needed!

๐Ÿ” What's happening?

  • celsius is a writable signal tied to user input.
  • fahrenheit is a computed signal that recalculates only when celsius changes.
  • Angular automatically tracks dependencies and re-runs computed() only when needed.

โš™๏ธ Why Signals Matter

Signals aren't just syntactic sugar. They represent a philosophical shift in how Angular handles reactivity:

  • Targeted updates: Only consumers that rely on changed signals get updated.
  • Automatic dependency tracking: Signals know who uses them, so changes ripple only where needed.
  • No more full tree checks: This drastically improves performance, especially in complex apps.
  • Lazy evaluation: Computed signals only recompute when accessed.
  • Interop with RxJS and legacy code: You can mix signals with the current system, easing migration.

๐Ÿ˜ฐ The Learning Curve

Yes โ€” this will be a shift for many developers.

We'll have new concepts to learn:

  • Signal-based inputs and outputs
  • Effects instead of lifecycle hooks
  • Possibly fewer decorators
  • A whole new way to think about state and reactivity

It might sound overwhelming, but like any meaningful evolution, it comes with a learning curve. Still, the payoff is worth it: more predictable change detection, better performance, and cleaner state management.

๐Ÿš€ The Road Ahead

This post was meant to give you a high-level overview of what Angular Signals bring to the table. You can find more detailed examples in Angular docs. Thanks for reading and happy coding!

Thank you for being a part of the community

Before you go: