Introduction
For years, Angular has depended on Observables and its Change Detection mechanism to handle reactivity. With the launch of Angular 16, a new reactive primitive called Signals was introduced, providing a more fine-grained, predictable, and efficient approach to managing state updates. Signals mark a significant evolution in Angular’s reactivity model, aiming to streamline state management while improving rendering performance and overall application efficiency
What Are Signals?
A Signal is a reactive container that holds a value and automatically notifies its dependents whenever that value changes. In contrast to Observables—which are push-based and typically asynchronous—Signals operate synchronously and provide more transparent state tracking.
Key Characteristics
Lightweight: They introduce very little overhead during creation and updates.
Fine-grained: Only the components or computations that depend on the changed value are updated, rather than triggering a full view refresh.
Predictable: State changes propagate immediately and in a consistent manner.
Compatible: They integrate smoothly with existing Angular features and APIs.
Why Angular Introduced Signals
Before Signals, Angular’s reactivity model leaned on:
Zone.js-driven change detection
@Input() property bindings
RxJS Observables
Manual state management techniques
While these tools were powerful, they came with notable drawbacks:
Entire component trees could re-render unnecessarily
Debugging reactive flows was often complicated
Performance tuning demanded deep expertise in change detection strategies
Signals solve these challenges by introducing fine-grained reactivity. Instead of re-rendering broadly, Angular now updates only the parts of the application that directly depend on the changed state.
Types of Signals
1) Writable Signals
Writable signals expose an API that allows you to modify their values directly. They are created by invoking the signal function and passing in the initial value as an argument.
Example:
import { signal } from '@angular/core';
const counter = signal(0);
counter.set(1); // updates value
counter.update(value => value + 1); // increments
Core Methods
| Method Name | Description |
|---|
| set(newValue) | The set() method replaces the current value of the signal with a new one. |
| update(fn) | The update() method modifies the signal’s value based on its current state. |
| asReadonly() | The asReadonly() method creates a read-only version of the signal. |
2) Computed Signals
Computed signals are reactive values that are automatically derived from one or more other signals. Instead of holding independent state, they act as pure functions of existing signals, recalculating only when their dependencies change.
Example:
import { signal, computed } from '@angular/core';
const price = signal(200);
const quantity = signal(4);
const total = computed(() => price() * quantity());
console.log(total()); // 800
If price or quantity changes, total is recalculated automatically. You don’t need to manually trigger updates—Angular handles the dependency tracking for you.
Important Behaviors
Lazy evaluation: Computed signals don’t run until they’re accessed.
Caching: Results are stored, so repeated reads don’t cause unnecessary recomputation.
Dependency awareness: They re-run only when one of their input signals changes.
Why They’re Useful
Simplify derived state (e.g., totals, filters, formatted values).
Reduce boilerplate compared to manual subscriptions or RxJS operators.
Improve performance by avoiding redundant recalculations.
Make reactive flows more predictable and easier to debug.
3) Effects
Effects are reactive functions that automatically run whenever the signals they depend on change. They’re designed to handle side effects—operations that go beyond pure state updates.
Example:
import { effect } from '@angular/core';
effect(() => {
console.log('Total changed:', total());
});
In this example, whenever total() changes, the effect executes and logs the new value.
Common Use Cases
Effects are ideal for tasks that need to respond to state changes but don’t belong in core business logic:
Logging application state changes
Triggering API calls when certain values update
Interacting with the DOM (e.g., animations, focus management)
Synchronizing state with local storage or external systems
Best Practices
Keep effects focused on side effects only.
Avoid embedding business logic inside effects—this should remain in signals or computed signals.
Prevent circular dependencies (e.g., an effect updating a signal that triggers the same effect again).
Signals vs Observables
| Feature | Signals | Observables |
|---|
| Nature | Synchronous reactive value holder | Asynchronous data stream |
| Data Flow | Pull-based (value is read directly) | Push-based (values are emitted to subscribers) |
| Primary Use Case | Local UI state management | Async operations (HTTP, events, streams) |
| Change Detection | Fine-grained updates (only dependent parts re-render) | Works with async pipe or manual subscription |
| Value Access | Always holds a current value | May or may not have a current value |
| Complexity | Simple and lightweight API | Powerful but involves operators and subscriptions |
| Subscription Required | No | Yes |
| Best For | Component state and derived state | Server calls, real-time data, event handling |
Advanced Concepts
1) Reactive Contexts
Signals automatically track dependencies through reactive contexts. When a Signal is accessed inside a computed() or effect(), Angular internally records that relationship. This means Angular knows exactly which computations or template bindings depend on which Signals. When a Signal changes, only the affected parts of the UI are updated — not the entire component. This fine-grained tracking leads to more efficient rendering and better performance compared to traditional change detection.
2) Equality Functions
By default, a Signal triggers updates whenever its value changes by reference. However, you can provide a custom equality function to control when updates should occur. This is useful when working with objects or arrays where structural comparison is preferred over reference comparison. A proper equality function can prevent unnecessary re-renders and improve performance in complex state scenarios
3) Integration with RxJS
Signals can interoperate with Observables using helper functions like toSignal() and toObservable()
Convert Observable -> Signal
Angular provides a convenient way to convert an Observable into a Signal, allowing you to integrate asynchronous streams with the new fine-grained reactivity model. Angular offers the toSignal() utility (from @angular/core/rxjs-interop) to convert an Observable into a Signal.
Example:
import { toSignal } from '@angular/core/rxjs-interop';
data$ = this.http.get<User[]>('/api/users');
dataSignal = toSignal(data$, { initialValue: [] });
When converting an Observable to a Signal, you must provide an initial value. Signals always need a current state, but Observables emit asynchronously. Supplying an initial value ensures the signal has a defined state right from the start, even before the first emission.
Convert Signal -> Observable
Angular provides the toObservable() utility (from @angular/core/rxjs-interop) to convert a Signal into an Observable. It creates an Observable that emits a new value whenever the Signal changes.
Example:
import { toObservable } from '@angular/core/rxjs-interop';
count$ = toObservable(this.countSignal);
This feature is especially handy when you need to apply RxJS operators like map, filter, or switchMap. It enables smooth integration of Signals into existing RxJS-based workflows, services, and third-party libraries.
Benefits of Signals
Performance: Minimizes unnecessary re-rendering by updating only the parts of the UI that depend on changed values.
Clarity: Clearly defines reactive dependencies, making data flow easier to understand and maintain.
Ease of Use: Eliminates the need for manual subscriptions and complex reactive patterns.
Future-Ready: Brings Angular in line with modern, fine-grained reactivity approaches.
Conclusion
Signals mark a significant advancement in Angular’s reactivity model. They offer a cleaner, more efficient, and predictable approach to handling state compared to earlier patterns. Although Observables are still crucial for managing asynchronous operations, Signals have become the preferred choice for local state handling and precise, fine-grained UI updates.