TL;DR: Angular Signals offers a synchronous and efficient way to manage state changes, leading to more responsive UIs. It’s a significant step toward a faster and more developer-friendly Angular framework.
Angular has long been known for its powerful reactive tools, particularly its use of Observables, which efficiently handle asynchronous data and event streams. However, the recent addition, Angular Signals, introduces a new approach to reactivity that simplifies synchronization between the application state and the UI.
Angular signals offers a fresh, synchronous way to manage state changes that enhances UI performance by targeting only the elements affected by data updates. This makes Signals particularly useful for optimizing change detection, reducing unnecessary updates, and allowing developers to maintain a more responsive interface.
In this blog, we’ll explore Angular Signals, how they differ from Observables, and how they integrate into Angular’s existing ecosystem. We’ll also cover practical use cases and how to leverage signals to build efficient, reactive apps.
Reactive programming is a programming paradigm designed to create software that automatically reacts to changes in data and events. Instead of waiting for user actions like clicks or keyboard inputs, reactive systems respond dynamically to shifts in the data they manage. This approach enables apps to remain responsive and up-to-date in real-time, providing smoother user experiences.
In the context of Angular, reactive programming refers to updating a component’s template in response to changes in its underlying data, such as data in the component’s class or services. For example, if a component’s data is updated via a service, Angular’s reactive programming principles ensure the view is automatically re-rendered to reflect this new data. By using reactive programming, Angular apps can maintain high responsiveness and efficiency, making them more intuitive and user-friendly.
In Angular, reactivity is handled primarily through zone.js, a library that enables Angular to detect when a change has occurred in the app. However, this detection is somewhat limited. Although zone.js identifies that something has changed, it does not specify what changed, where the change occurred, or how it impacts different parts of the app. As a result, Angular is only aware that some change has occurred without further details about which specific elements or parts of the view are affected.
To manage these changes, Angular initiates a process called change detection. During this process, it traverses the entire component and view tree, checking all data bindings to see if anything has been updated. This approach ensures that all changes are captured but can be inefficient, as Angular will check every binding—even those that have not changed—resulting in unnecessary processing.
To improve performance, Angular offers the OnPush change detection strategy, limiting change detection to only those components with specific triggers, such as updated input properties or observable data streams. OnPush allows Angular to skip parts of the view tree that haven’t changed, optimizing the process. However, many components are still subject to change detection, which can create overhead in larger apps.
Additionally, in development mode, Angular runs change detection twice, aiming to detect unintended side effects from data updates. While helpful for identifying bugs, double-checking adds to the processing load, potentially impacting development efficiency. This approach highlights both the strengths and limitations of Angular’s reactive system, emphasizing the need for optimizations like OnPush to maintain performance as apps grow in complexity.
To improve Angular’s performance and developer experience, several goals guide the evolution of the framework:
These improvements aim to make Angular a faster, more efficient, and developer-friendly framework that can handle the growing demands of modern web apps.
Angular signals are a new reactive primitive introduced in Angular 16 that provides a powerful and efficient way to manage state and reactivity in your apps. Angular signals introduce a new way of managing reactivity in apps, focusing on precision and efficiency. A signal is essentially a wrapper around a value that notifies any parts of the app relying on it whenever that value changes. Unlike other reactive constructs, a signal always holds a value, which can be of any type—ranging from simple data like numbers or strings to complex objects or arrays.
One key characteristic of signals is that they are side-effect-free; reading a signal does not trigger any other code or create unintended changes. This makes signals highly predictable and safe to use, as developers can trust that accessing a signal won’t accidentally alter different parts of the app. Signals track changes with precision, updating only the parts of the UI directly affected, in contrast to the zone.js approach where Angular runs change detection across the entire component tree to capture updates.
As of Angular 18, the change detection system still depends on zone.js, which limits the immediate performance benefits of signals. Although using signals doesn’t yet provide a substantial speed increase compared to the existing zone.js-based reactivity, signals represent an important step forward. They set the stage for zoneless app in the future, paving the way for a more efficient, streamlined approach to managing reactivity in Angular.
Angular signals offer three main types of reactive constructs:
Each type serves a different purpose in managing data reactivity within an Angular app. Let’s explore them in detail.
These fundamental, mutable signals hold a value and allow for updates. Writable signals act as a state container that can store any kind of data—whether it’s a number, string, object, or array. Developers can create a writable signal, set an initial value, and update it as needed. When the value of a writable signal changes, it automatically notifies every part of the UI that depends on it, causing only those parts to update. Signals are getter functions; calling them will return the current value of the signal.
Angular has two primary methods for changing a signal’s value: set() and update(). Each serves a unique purpose:
Example:
import { signal } from '@angular/core'; // Writable signal initialized with a value of 0. const counter = signal(0); // Signals are getter functions, calling them will return the current value of the signal. console.log('Count is: ', counter()); // 0 // Set the value of a signal. counter.set(3); // Update the value of a signal. counter.update((value) => value + 1);
Computed signals are read-only signals that derive their values from other signals, making them perfect for calculations or transformations that depend on existing state. Computed signals are created using the computed function, which defines a derivation based on one or more dependent signals.
Here’s a breakdown of their key characteristics:
For example, we can use a computed signal to double the value of a writable signal, as shown in the following code.
import { signal, computed } from '@angular/core'; // Writable signal with an initial value of 5. const counter = signal(5); // Automatically doubles the value. // doubleCounter will be 10, and it updates automatically if the counter changes. const doubleCounter = computed(() => counter() * 2);
In this example, doubleCounter depends on the counter signal. If the counter changes, doubleCounter will automatically recalculate its value without any direct assignment, providing a streamlined, reactive way to derive values in Angular apps. Computed signals are powerful for keeping derived data in sync and are integral to Angular’s approach to precise, efficient reactivity.
Effects allow developers to respond to changes in signals by invoking side effects, like making API requests, logging, or updating the DOM. Effects are created using the effect function and automatically rerun whenever any of their dependent signals change.
Here’s a deeper look at their key characteristics:
Suppose we have a userId signal and an effect that fetches user data every time when the userId changes:
import { signal, effect } from '@angular/core'; const userId = signal(1); effect(() => { console.log(`Fetching data for user ID: ${userId()}`); // Make an API call or any other side effect here. });
In this example, the effect will log a message whenever userId is updated. The dynamic dependency tracking allows the effect to rerun automatically for each change in userId, ensuring the side effect stays in sync with the latest data.
Effects are invaluable for performing tasks that interact with the outside world or rely on asynchronous operations. They provide a clean, reactive approach to handling side effects in Angular.
While effects in Angular signals may not be necessary for every app, they shine in specific scenarios where side effects need to be managed in response to reactive state changes. Here are some instances where using impacts can be particularly beneficial:
While effects offer valuable functionality in Angular signals, there are certain scenarios where their use should be avoided to maintain a stable and efficient app. Here are key considerations for when not to use effects:
Signals and observables serve different purposes in managing data flow within an Angular app. Signals are a synchronous mechanism that Angular uses to manage and synchronize the app state with the UI. Their synchronous nature makes them ideal for handling real-time state changes predictably. When a signal’s value changes, it automatically updates the associated UI components. This approach enhances Angular’s change detection by updating only the parts of the UI that need it, making state management more efficient and responsive.
On the other hand, observables are asynchronous, making them highly suited for tasks that involve waiting for external resources or handling delayed responses, like fetching data from APIs. By design, observables can emit multiple values over time, making them perfect for handling streams of data or events asynchronously. This capability is especially useful when handling user interactions or data that updates at unpredictable intervals.
With Angular signals utility functions, developers can integrate signals and observables within the same app. These utilities enable developers to harness the synchronous nature of signals alongside the asynchronous advantages of observables. This integration provides a streamlined way to leverage the strengths of both approaches, allowing for flexible and reactive programming tailored to specific application needs.
Angular provides a convenient way to convert observables into signals using the toSignal function. This function allows developers to harness the reactivity of observables in a more flexible way than the traditional async pipe. Unlike the async pipe, which is limited to template use, toSignal can be applied anywhere in an app, giving developers greater control over where and how reactive data is handled.
The toSignal function begins by subscribing to the observable immediately. This immediate subscription means that any emissions from the observable trigger updates in the signal’s value right away. However, developers should be mindful that this immediate subscription can also initiate side effects, such as HTTP requests or other operations defined in the observable.
Another essential aspect of toSignal is its built-in cleanup mechanism. When a component or service using toSignal is destroyed, Angular automatically unsubscribes from the observable. This automatic cleanup prevents potential memory leaks by ensuring that unused subscriptions are closed, which is particularly helpful in large or complex apps where managing subscriptions manually can be challenging. By using toSignal, developers can create reactive and efficient apps while maintaining clean and manageable code.
Example:
import { toSignal } from '@angular/core/rxjs-interop'; private readonly httpClient = inject(HttpClient); private products$ = this.httpClient.get<ProductResponse>('api/endpoint/'); // Get a `Signal` representing the `products$`'s value. productList = toSignal(this.products$);
Angular provides a toObservable function to convert signals into observables, making it easy to leverage signals in reactive contexts where observables are required. This conversion is particularly useful when you need to work with other libraries or components that expect data in the form of an observable.
The toObservable function operates by monitoring the signal’s value through an effect. Whenever the signal’s value changes, toObservable captures these changes and emits them as observable values. However, to prevent unnecessary emissions, toObservable only emits the final, stabilized value when a signal updates multiple times in quick succession. This ensures that only meaningful changes are propagated, keeping the observable stream efficient and avoiding redundant processing.
Example:
fruitList = signal(['apple', 'orange', 'grapes']); fruitList$: Observable<string[]> = toObservable(this.fruitList);
Let’s understand the concept of signals using a real-world app. We will create a mini version of an e-commerce app.
Run the following command to create an Angular app.
ng new signals-demo
Run the following command and follow the on-screen instructions to set up Angular Material for your project.
ng add @angular/material
Add a new folder named models in the src\app folder. Create a new file called product.ts inside the models folder and add the following code.
export interface Product { id: number; title: string; images: string[]; price: number; } export interface ProductResponse { limit: number; skip: number; total: number; products: Product[]; }
Add a file called shopping-cart.ts inside the models folder and put the following code inside it.
import { Product } from './product'; export interface ShoppingCart { product: Product; quantity: number; }
Add a new service file using the following command.
ng g s services\product
Update the ProductService class in the \app\services\product.service.ts file, as shown.
export class ProductService { private readonly httpClient = inject(HttpClient); private products$ = this.httpClient.get<ProductResponse>( 'https://dummyjson.com/products?limit=8' ); cartItemsList = signal<Product[]>([]); productList = toSignal(this.products$); }
The cartItemsList is a public property that holds the signal of an empty array of Product objects. The productList is a public property with a signal converted from the products$ observable.
The service fetches a list of products from a publicly available API and provides reactive signals for both the product and cart items lists.
Create a new component using the following command.
ng g c components\product-list
Update the ProductListComponent class in the src\app\components\product-list\product-list.component.ts file as follows.
export class ProductListComponent { private readonly productService = inject(ProductService); protected productList = this.productService.productList; addItemToCart(id: number) { const selectedItem = this.productList()?.products.find( (product) => product.id === id ); this.productService.cartItemsList.update((cartItems) => { if (selectedItem) cartItems?.push(selectedItem); return cartItems; }); } }
The ProductListComponent is responsible for displaying a list of products and allowing users to add products to their cart. The addItemToCart function searches for a product with a specific ID in the product list. On finding the product, it adds it to the cart items list and updates it to include the newly added product.
Add the following code in the src\app\components\product-list\product-list.component.html file.
<div class="row"> <div class="col d-flex justify-content-start mb-4 flex-wrap my-2"> @for (product of productList()?.products; track $index) { <mat-card class="product-card m-2" appearance="outlined"> <img class="preview-image" mat-card-image src="{{ product.images[0] }}" alt="product image" /> <mat-card-content> <p>{{ product.title }}</p> </mat-card-content> <mat-card-actions class="mt-2" align="end"> <button mat-flat-button (click)="addItemToCart(product.id)"> Add to Cart </button> </mat-card-actions> </mat-card> } </div> </div>
This template renders a list of products in a responsive grid layout using Angular Material cards. Each card displays a product image, title, and an Add to Cart button. The @for directive loops through the products and dynamically generates the cards.
Create the cart component using the following command.
ng g c components\cart
Update the CartComponent class in the src\app\components\cart\cart.component.ts file as shown:
export class CartComponent { private readonly productService = inject(ProductService); displayedColumns: string[] = ['name', 'quantity', 'price']; protected finalCartItems = computed(() => { const shoppingCart: ShoppingCart[] = []; this.productService.cartItemsList().map((item) => { const index = shoppingCart.findIndex( (finalCart) => item.id === finalCart.product.id ); if (index > -1) { shoppingCart[index].quantity += 1; } else { shoppingCart.push({ product: item, quantity: 1 }); } }); return shoppingCart; }); protected totalCost = computed(() => { return this.finalCartItems().reduce((acc, item) => { return acc + item.product.price * item.quantity; }, 0); }); }
The finalCartItems computed signal aggregates the cart items by combining quantities for items with the same ID. Meanwhile, the totalCost computed signal calculates the total cost of the items in the cart by summing the product of the price and quantity for each item.
Add the following code in the src\app\components\cart\cart.component.html file.
<div class="row d-flex justify-content-center"> <div class="col-md-10 my-4"> @if(finalCartItems().length > 0){ <mat-card> <mat-card-content> <table class="mat-display-1" mat-table [dataSource]="finalCartItems()"> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef>Name</th> <td mat-cell *matCellDef="let element"> {{ element.product.title }} </td> <td mat-footer-cell *matFooterCellDef></td> </ng-container> <ng-container matColumnDef="quantity"> <th mat-header-cell *matHeaderCellDef>Quantity</th> <td mat-cell *matCellDef="let element">{{ element.quantity }}</td> <td mat-footer-cell *matFooterCellDef>Total Cost</td> </ng-container> <ng-container matColumnDef="price"> <th mat-header-cell *matHeaderCellDef>Price</th> <td mat-cell *matCellDef="let element"> {{ element.product.price | currency }} </td> <td mat-footer-cell *matFooterCellDef> <strong>{{ totalCost() | currency }}</strong> </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> <tr mat-footer-row *matFooterRowDef="displayedColumns"></tr> </table> </mat-card-content> </mat-card> }@else { <h3>No data found</h3> } </div> </div>
This template renders a shopping cart using Angular Material components. It conditionally displays a table of cart items if there are any items in the cart, showing the product name, quantity, and price. If the cart is empty, it displays a No data found message. The table includes header, data, and footer rows, with the footer displaying the total cost of the items in the cart.
Create the nav-bar component using the following command.
ng g c components\nav-bar
Update the NavBarComponent class in the src\app\components\nav-bar\nav-bar.component.ts file:
export class NavBarComponent { private readonly productService = inject(ProductService); cartItemsList = this.productService.cartItemsList; }
Add the following code in the src\app\components\nav-bar\nav-bar.component.html file.
<mat-toolbar> <button mat-button [routerLink]="['/']"> <mat-icon aria-hidden="false" fontIcon="home_filled"></mat-icon> Home </button> <span class="spacer"></span> <button mat-icon-button matBadge="{{ cartItemsList().length }}" matBadgeSize="large" class="me-4" > <mat-icon [routerLink]="['/cart']">shopping_cart </mat-icon> </button> </mat-toolbar>
This template creates a navigation bar with home and cart buttons. The home button navigates to the home page and displays a home icon. The cart button shows a shopping cart icon and a badge with the number of items in the cart.
Update the routing in the app.route.ts file.
import { Routes } from '@angular/router'; import { ProductListComponent } from './components/product-list/product-list.component'; import { CartComponent } from './components/cart/cart.component'; export const routes: Routes = [ { path: '', component: ProductListComponent }, { path: 'cart', component: CartComponent }, ];
Update the src\app\app.component.html file as shown.
<app-nav-bar></app-nav-bar> <div class="container"> <router-outlet></router-outlet> </div>
Finally, run the app using the command ng serve. You can see the output here.
You can refer to the complete code example on GitHub.
Angular signals bring simplicity and precision to reactive programming in Angular apps. By providing a synchronous, efficient way to manage state, signals allow developers to update the UI only when necessary, improving performance by minimizing redundant change detection cycles. Unlike observables, which are ideal for handling asynchronous data streams, signals excel in synchronizing app state with the UI, making them a powerful addition for managing real-time data in a responsive interface.
Throughout this blog, we explored how signals fit into Angular’s reactive ecosystem, the differences between signals and observables, and when to use each approach. We also discussed practical use cases for signals, such as working with computed values and effects and integrating signals with observables for a flexible and efficient reactivity model.
With Angular signals, developers now have greater control over app reactivity, setting a solid foundation for zoneless apps and more streamlined change detection in the future. As Angular continues to evolve, mastering signals will become essential for building high-performance, modern apps.