🅰️ Angular Q81 / 128

How does signal-based reactivity work in Angular?

AI-Powered Answer ✓ Answered

Angular's signal-based reactivity system provides a new, more efficient, and fine-grained approach to managing state and triggering updates in applications. It moves away from the traditional Zone.js-based change detection for reactive updates, offering better performance and a clearer mental model for data flow.

What are Signals?

Signals are reactive primitives in Angular that hold a value and notify consumers when that value changes. They are essentially wrapper objects around a value that can tell Angular (or anything else) when its value has been updated, allowing for highly targeted and efficient updates to the UI or other parts of the application.

Core Concepts

The signal API revolves around three main functions: signal(), computed(), and effect().

Creating a Signal (`signal()`)

signal() creates a writable signal. You can read its current value by calling it like a function (e.g., mySignal()) and update its value using .set() or .update() methods.

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

const count = signal(0);

console.log(count()); // 0

count.set(5);
console.log(count()); // 5

count.update(currentValue => currentValue + 1);
console.log(count()); // 6

Derived Signals (`computed()`)

computed() creates a read-only signal whose value is derived from one or more other signals. It automatically re-evaluates its value only when its dependencies (the signals it reads) change. This provides memoization, ensuring efficiency.

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

const firstName = signal('John');
const lastName = signal('Doe');

const fullName = computed(() => `${firstName()} ${lastName()}`);

console.log(fullName()); // "John Doe"

firstName.set('Jane');
console.log(fullName()); // "Jane Doe" (re-evaluated automatically)

Side Effects (`effect()`)

effect() registers a side-effect function that runs whenever one of its dependencies (signals read within the effect) changes. Effects are useful for non-rendering logic, like logging, updating the DOM outside of templates (e.g., using a third-party library), or synchronizing with browser APIs. They are generally discouraged for modifying application state.

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

const username = signal('user123');

effect(() => {
  console.log(`Username changed to: ${username()}`);
});

username.set('newuser456'); // Logs "Username changed to: newuser456"

How Signals Enhance Reactivity

  • Fine-grained Updates: When a signal's value changes, Angular knows precisely which components, templates, or effects depend on that signal. Only the affected parts are updated, rather than re-checking an entire component tree.
  • Zone.js Independence: For signal-driven updates, Angular's change detection no longer relies on Zone.js. This simplifies the change detection mechanism and can lead to significant performance improvements.
  • Explicit Dependencies: The reactivity graph is built explicitly by computed and effect functions, making it easier to reason about data flow and predict when updates will occur.
  • Performance: By minimizing unnecessary re-renders and reducing the overhead of Zone.js, signals contribute to better application performance, especially in complex applications with frequent state changes.

Integration with Components and Templates

Signals can be used directly within Angular components and templates. In a template, you access a signal's value by calling it like a function (e.g., count()). When the signal's value changes, Angular's rendering engine automatically updates the relevant parts of the DOM.

typescript
// my-component.component.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-my-component',
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double Count: {{ doubleCount() }}</p>
    <button (click)="increment()">Increment</button>
  `,
  standalone: true
})
export class MyComponent {
  count = signal(0);
  doubleCount = signal(0);

  constructor() {
    // A computed signal for doubleCount
    this.doubleCount = computed(() => this.count() * 2);
  }

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

Key Benefits

  • Predictable Performance: More deterministic and potentially faster rendering due to fine-grained updates.
  • Simplified Reactivity: A clearer and more explicit way to manage reactive state compared to relying solely on Zone.js.
  • Improved Developer Experience: Easier to debug and reason about change detection and data flow.
  • Future-Proofing: Aligns Angular with modern web reactivity patterns and prepares for potential future removal of Zone.js as a default dependency.