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”.
- Global Change Detection: Angular starts a traversal from the root component to the last child (down the tree), comparing current values with previous ones.
- The Problem: While effective, it is inefficient in large applications. Angular doesn’t know exactly what changed; it only knows that something changed, so it has to re-evaluate too many things. This is known as Dirty Checking.
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).
- Producer: The Signal that changes.
- Consumer: The component or template that reads the Signal.
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
| Feature | Traditional Flow (Zone.js) | Reactive Flow (Signals) |
|---|---|---|
| Detection | Based on async events. | Based on specific state changes. |
| Granularity | Top-down (Full tree). | Fine-grained (Only affected parts). |
| Performance | Consumes 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:
- Lighter Bundle Size: By removing Zone.js, your application weighs less from the start.
- Better Core Web Vitals: By not blocking the main thread with global change detections, interaction is smoother (INP - Interaction to Next Paint).
- Interoperability with RxJS: They are not here to replace RxJS, but to complement it. You use RxJS for complex asynchronous flows (streams) and Signals for pure UI state.
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.
- Benefit: You forget about memory leaks because
toSignalhandles subscription and unsubscription automatically according to the component’s lifecycle. - Behavior: Since a Signal must always have a value, you can provide an
initialValue.
// 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.
- Use case: You have a
searchQuery()Signal. Every time it changes, you want to make an HTTP request, but you want to useswitchMapto cancel previous requests.
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.
| Scenario | Ideal Tool | Reason |
|---|---|---|
| UI State | Signals | Synchronous, simple, no manual subscriptions, and fine-grained performance. |
| Async Events | RxJS | Time management, retries, cancellation, and stream composition. |
| Data Derivation | Signals (computed) | Executed lazily and only when necessary. |
| API Orchestration | RxJS | Operators 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:
- Analytics logs.
- Saving data to
localStorage. - Manual DOM manipulations (if strictly necessary).
Watch out: Do not use
effect()to change other states (Signals), as you could create infinite cycles. Usecomputed()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.