Search

Back to all posts

The Rebirth of Angular: From Components to Signals

Avatar
Emmanuel Luján
6 min read

The Rebirth of Angular: From Components to Signals

Table of Contents

Open Table of contents

The Zone.js Era: Magic at a Cost

Historically, Angular has relied on Zone.js to know when to update the view. This isn’t necessarily bad, but it has its drawbacks. Throughout this post, we will deeply analyze the reasons behind this shift.

How does the traditional flow work?

Zone.js creates a “context” around all asynchronous operations (clicks, HTTP requests, timeouts). When something happens, Zone.js notifies Angular: “Hey, something happened, check everything”.


The Rise of Signals: Fine-Grained Reactivity

Signals (introduced starting from v16) change the paradigm: instead of Angular “guessing” if there were changes, the data itself notifies Angular.

What is a Signal?

Imagine a Signal as a “container” of a value that maintains a list of who is using it (subscribers).

Internal Mechanics: The Dependency Graph

Unlike RxJS Observables, Signals do not require manual subscriptions (avoiding the | async pipe or .subscribe() calls). Angular automatically creates a dependency graph. If a Signal changes, Angular knows exactly which part of the DOM needs to be updated without having to traverse the entire component tree.


Technical Comparison: Traditional Flow vs. Signals

FeatureTraditional Flow (Zone.js)Reactive Flow (Signals)
DetectionBased on async events.Based on specific state changes.
GranularityTop-down (Full tree).Fine-grained (Only affected parts).
PerformanceConsumes more CPU in complex apps.Optimized; avoids unnecessary comparisons.
DX (Dev Experience)More “magical” but harder to debug.More explicit and predictable.

Why is it the Current Trend? (The Road to Zoneless)

Angular’s ultimate goal is to become Zoneless. By using Signals, we can eventually remove Zone.js from our polyfills.ts.

Advantages of this trend:


Code Implementation

Implementing a signal is very simple; you just use Angular’s signal() function. Below is an example of how to implement a signal in Angular.

// Before (Traditional)
count = 0;
increment() { this.count++; }

// Now (Signals)
count = signal(0);
doubleCount = computed(() => this.count() * 2); // Updates only if count changes

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

The Synergy: RxJS + Signals

While Signals handle reactivity in the view amazingly well, RxJS remains the king for managing complex asynchronous flows (such as HTTP requests, WebSockets, or input debouncing). The key lies in the @angular/core/rxjs-interop package.

1. From Observable to Signal: toSignal()

This is the most common case: you receive data from an API (Observable) and want to display it in the UI efficiently without using the async pipe.

// In your service or component
users$ = this.http.get<User[]>('/api/users');
users = toSignal(this.users$, { initialValue: [] }); 

// In the template, you no longer use the async pipe:
// <ul> @for (user of users(); track user.id) { ... } </ul>

2. From Signal to Observable: toObservable()

Sometimes you need to react to a Signal change to trigger an asynchronous side effect, such as a database search every time a filter changes.

query = signal('');
// We convert the signal to an observable to use RxJS operators
results$ = toObservable(this.query).pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(q => this.userService.search(q))
);

When to Use What?

It is of utmost importance to know when to use each of these tools, as they are not mutually exclusive but complementary. Below is a comparison table to help you make the best decision.

ScenarioIdeal ToolReason
UI StateSignalsSynchronous, simple, no manual subscriptions, and fine-grained performance.
Async EventsRxJSTime management, retries, cancellation, and stream composition.
Data DerivationSignals (computed)Executed lazily and only when necessary.
API OrchestrationRxJSOperators like combineLatest or forkJoin are much more powerful for this.

The Pro Concept: effect() vs tap()

In RxJS, we use tap() for side effects within a stream. In the world of Signals, we have effect().

An effect() runs at least once and then every time any Signal read inside it changes. It is perfect for:

Watch out: Do not use effect() to change other states (Signals), as you could create infinite cycles. Use computed() for that.


Conclusion

Signals did not kill RxJS; on the contrary, they freed it from the burden of managing UI rendering, allowing it to shine in what it does best: managing complex data flows over time.

Signals are not just a new “feature,” but the foundation upon which Angular is being rebuilt to compete in performance with frameworks like Solid.js or Qwik, while maintaining its enterprise robustness and the solid architecture that makes it the choice for large-scale applications.