Skip to content
sg.Siaka Gassama
All writing

Reactivity in Angular: Signals vs Observables

Angular applications often deal with asynchronous data, making reactive programming a powerful tool for managing data flow and building responsive UIs. Within this paradigm, two key concepts emerge: Signals and Observables (the latter powered by RxJS). While both handle data streams, they have distinct characteristics that influence when to choose one over the other. This blog delves into their functionalities, strengths, and weaknesses, guiding you towards the ideal choice for your Angular project.

Understanding Reactive Programming

Reactive programming is a programming paradigm that focuses on data streams and the reactions they trigger. Imagine a river; data flows downstream, and components along the riverbank react to changes in the water level (data). Observables are the fundamental building blocks of reactive programming in Angular. They represent streams of values that can be emitted over time, allowing components to subscribe and react to these emissions.

Enter Signals: A Simpler Approach

Introduced in Angular version 16, Signals offer a lighter alternative to RxJS Observables. They act as wrappers around a value, notifying interested components whenever the value changes. Signals are designed to be simpler to use, especially for common UI-related asynchronous operations within Angular.

Key Differences

While both Signals and Observables handle data streams, they differ in crucial aspects:

  • Complexity: Signals boast a simpler API with a focus on basic value changes. RxJS Observables, on the other hand, offer a rich set of operators for manipulating, transforming, and combining data streams, making them more complex but also more powerful.
  • Subscription Management: Signals automatically manage subscriptions within the Angular change detection cycle. This simplifies code but limits control over when and how components receive data. Observables require manual subscription management, offering more granular control over the data flow but adding complexity.
  • Emission Style: Signals can only emit a single value at a time. RxJS Observables can emit multiple values over time, making them suitable for representing sequences of data.
  • Completion and Error Handling: Signals lack built-in mechanisms for handling completion or errors within the data stream. RxJS Observables provide operators for explicitly signaling completion and handling errors, crucial for robust asynchronous operations.

Choosing the Right Tool: Signals vs Observables

The decision between Signals and Observables hinges on your project's requirements:

  • Simple UI Updates: For straightforward value changes within the UI, like updating a loading indicator or displaying fetched data, Signals shine with their ease of use.
  • Asynchronous Operations: When dealing with complex asynchronous operations like HTTP requests or handling user interactions that require data transformations, RxJS Observables offer the necessary power and flexibility with their rich operator set.
  • Fine-Grained Control: If you need precise control over data flow, including subscription management, error handling, and data manipulation, RxJS Observables provide the tools you need.
  • Project Familiarity: If your team is already comfortable with RxJS, continuing with Observables might be a smoother transition. On the other hand, if you're looking for a gentler introduction to reactive programming, Signals offer a good starting point.

Examples

1. Updating a counter based on user clicks using a Signal:

Simple Value Change (Signal)
1
2import { Component } from '@angular/core';
3import { Signal } from '@angular/core';
4
5@Component({
6 selector: 'app-counter',
7 template: `
8 <button (click)="increment()">Increment</button>
9 <span>{{ count }}</span>
10 `
11})
12export class CounterComponent {
13 count = 0;
14 countChanged = new Signal<number>(this.count);
15
16 increment() {
17 this.count++;
18 this.countChanged.emit(this.count);
19 }
20}
21
Explanation:
  • We define a count variable to store the counter value.
  • We create a countChanged Signal initialized with the initial count value.
  • The increment function increases the count and emits the new value through the countChanged Signal.
  • The template displays the current count value using property binding.

2. Utilizing RxJS Observables to implement a debounced search with error handling:

Debounced Search with Error Handling (Observable)
1
2import { Component } from '@angular/core';
3import { fromEvent, debounceTime, distinctUntilChanged, tap, catchError } from 'rxjs/operators';
4import { of } from 'rxjs';
5
6@Component({
7 selector: 'app-search',
8 template: `
9 <input type="text" [(ngModel)]="searchTerm">
10 <div *ngIf="searchResults.length > 0">Results: {{ searchResults }}</div>
11 <div *ngIf="error">Error: {{ error }}</div>
12 `
13})
14export class SearchComponent {
15 searchTerm = '';
16 searchResults: any[] = [];
17 error: string | null = null;
18
19 ngOnInit() {
20 fromEvent(this.searchInputRef.nativeElement, 'keyup')
21 .pipe(
22 map(event => (event.target as HTMLInputElement).value),
23 debounceTime(400),
24 distinctUntilChanged(),
25 tap(() => this.error = null), // Clear previous error before search
26 catchError(error => {
27 this.error = error.message;
28 return of([]); // Emit an empty array to prevent unexpected behavior
29 })
30 )
31 // preferably use async pipes, but not doing so for brevity
32 .subscribe(searchTerm => this.fetchSearchResults(searchTerm));
33 }
34}
35
Explanation:
  • We define searchTerm, searchResults, and error variables.
  • The ngOnInit function creates an Observable from keyup events on the search input.
  • The debounceTime operator waits 400ms after each keystroke before emitting.
  • distinctUntilChanged avoids redundant searches for the same term.
  • tap is used to handle logic with side-effects and clears any previous error before a new search.
  • catchError captures errors during the search and updates the error property.
  • We use of to emit an empty array in case of error, preventing unexpected behavior from the fetchSearchResults function (not shown here).

These examples showcase the core differences:

  • Signals: Simpler API for basic value changes. Automatic subscription management within the Angular change detection cycle. Limited functionality.
  • Observables (RxJS): Rich operator set for data manipulation, transformation, and combination. Manual subscription management for precise control. Ability to handle completion and errors.
  • The debounceTime operator waits 400ms after each keystroke before emitting.
  • distinctUntilChanged avoids redundant searches for the same term.
  • tap is used to handle logic with side-effects and clears any previous error before a new search.
  • catchError captures errors during the search and updates the error property.
  • We use of to emit an empty array in case of error, preventing unexpected behavior from the fetchSearchResults function (not shown here).

Conclusion

In conclusion, signals and observables are powerful tools in the toolkit of Angular developers, offering distinct advantages and use cases. Signals are best suited for scenarios where direct control over data emission is required, such as event handling and state management. Observables, on the other hand, excel at handling asynchronous data streams and performing complex transformations. By understanding the differences between signals and observables, and knowing when to use each approach, developers can build more robust and maintainable Angular applications. Whether it's handling user interactions, making HTTP requests, or managing application state, signals and observables provide the building blocks for creating responsive and interactive user experiences in Angular.

Thanks for reading.Explore more articles →