Should You Pair Signals & OnPush?
Tags: angular, frontend, javascript, webdev
Published: 2023-09-21 16:56:09

When building a web application, there are different aspects that we take into consideration, **performance** being one of them. Especially when working on a considerable Angular codebase, there is always space for performance improvements.

An inherited codebase is what I have been working on recently, and the main thing to be improved was the performance of the application — refactoring to libraries, smart and dumb components, and starting to utilize the `OnPush` change detection strategy amongst other improvements.

In this article, I want to share the issue we faced while sparingly adding the `OnPush` strategy to components. Additionally, I will elaborate on a few solutions that are already known that we can use, the latest one being the future, new reactive primitive, Angular Signals.

## **The OnPush Change Detection**

Change Detection is the mechanism that makes sure that the application state is in sync with the UI. At a high level, Angular walks your components from top to bottom, looking for changes. The way it does, this is by comparing each current value of expressions in the template with the previous values they had using the **strict equality comparison operator** (**===**). This is known as **dirty checking**.
>  *You can read more about in the [official documentation](https://angular.io/guide/change-detection-skipping-subtrees).*

Even though change detection is optimized and performant, in applications with large component trees, running change detection across the whole app too frequently may cause **slowdowns** and **performance** issues. This can be addressed by using the `OnPush` change detection strategy, which tells Angular to never run the change detection for a component unless:

* At the time the **component is created.**

* The **component is dirty.**

>  There are actually 3 criteria when OnPush CD runs, and you can find more about them in this [article](https://blog.simplified.courses/angular-change-detection-onpush-or-not/).

This allows to skip change detection in an entire component subtree.

**The Problem:**

To better understand the issue, below is a small reproduction of the app supporting our case:

```
@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, RouterLink, RouterOutlet],
  template: `
    <h1>OnPush & Signals</h1>
    <a routerLink="/">Home</a> &nbsp;
    <a routerLink="/products">Products </a>
    <hr >

    <router-outlet></router-outlet>
  `,
})
export class AppComponent {
  name = 'OnPush & Signals';
}

bootstrapApplication(App, {
  providers: [
    provideHttpClient(),
    provideRouter([
      {
        path: '',
        component: HomeComponent,
      },
      {
        path: 'products',
        loadComponent: () =>
          import('./products-shell/products-shell.component'),
        children: [
          {
            path: '',
            loadChildren: () => import('./products').then((r) => r.routes),
          },
        ],
      },
    ]),
  ],
});
```
Using new standalone APIs in Angular, an application is bootstrapped with **HttpClient** and **Router** configured. The application has 2 routes configured, the default one for **HomeComponent**, and the 'products' for the **Product** feature (in our case being an Nx library) which is lazily-loaded when the route is activated and rendered in the lazily-loaded **ProductShell** component:

```
@Component({
  selector: 'app-products-shell',
  standalone: true,
  imports: [RouterOutlet],
  changeDetection: ChangeDetectionStrategy.OnPush, // configure OnPush
  template: `
    <header>
      <h2>Products List</h2>
    </header>

    <router-outlet></router-outlet>
  `,
  styleUrls: ['./products-shell.component.css'],
})
export default class ProductsShellComponent { }
```
The **Product** feature itself has the following route configuration:

```
export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./products.component'),
    children: [
      {
        path: 'list',
        loadComponent: () => import('./products-list/products-list.component'),
      },
      {
        path: '',
        redirectTo: 'list',
        pathMatch: 'full',
      },
    ],
  },
];
```
Let’s first have a look at the way it should not be done, and then check the solutions:

## Illustrating the problem

The **ProductList** component below calls the `getProducts` function inside the `ngOnInit` hook to get the list of products and then render it into a table.

```
@Component({
  selector: 'app-products-list',
  standalone: true,
  imports: [NgFor],
  template: `
    <table>
      <thead>
        <tr>
          <th>Title</th>
          <th>Description</th>
          <th>Price</th>
          <th>Brand</th>
          <th>Category</th>
        </tr>
      </thead>

      <tbody>
        <tr *ngFor="let product of products">
          <td>{{ product.title }}</td>
          <td>{{ product.description }}</td>
          <td>{{ product.price }}</td>
          <td>{{ product.brand }}</td>
          <td>{{ product.category }}</td>
        </tr>
      </tbody>
    </table>
  `,
  styleUrls: ['./products-list.component.css'],
})
export default class ProductsListComponent implements OnInit {
  products: Product[] =[];
  productService = inject(ProductsService);

  ngOnInit() {
    this.productService.getProducts().subscribe((products) => {
      this.products = products;
    });
  }
}
```
It will be rendered inside the **Products** component which wraps the `<router-outlet>` inside a `div` for page spacing purposes (in our case):

```
@Component({
  selector: 'app-products',
  standalone: true,
  imports: [RouterOutlet],
  template: `
   <div class="main-content">
    <router-outlet></router-outlet>
   </div>
  `,
  styles: [`.main-content { margin-top: 15px }`],
})
export default class ProductsComponent {}
```
At first sight, this code seems correct, but no products will be rendered on the table, and no errors in the console.


![Navigating from home page to the product list page when no data is rendered because of the change detection issue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fckx9owst5iflq9jys50.gif)

What could be happening? 🤯

This happens because of **ProductShell** (root) component is being configured using OnPushchange detection, and the "imperative way" of retrieving the products list. The products list is retrieved successfully, and the data model is changed, marking the **ProductsList** component as dirty, but not its ancestor components. Marking **ProductShell**  `OnPush` skips all subtree of components from being checked for change unless it is marked dirty, hence data model change is not reflected on UI.
Now that we understand what the issue is, there are a few ways that can solve it. Of course, the easiest one is just reverting to the **Default** change detection and everything works. But let's see what are the other solutions out there:

**Solution 1: Declarative Pattern with AsyncPipe**

Instead of imperatively subscribing to the `getProducts` function in the component, we subscribe to it in the template by using the `async` pipe:

```
@Component({
  ...
  template: `
    <table>
      ...
      <tbody>
        <tr *ngFor="let product of products$ | async">
          <td>{{ product.title }}</td>
          <td>{{ product.description }}</td>
          <td>{{ product.price }}</td>
          <td>{{ product.brand }}</td>
          <td>{{ product.category }}</td>
        </tr>
      </tbody>
    </table>
  `
})
export default class ProductsListComponent {
  productService = inject(ProductsService);
  products$ = this.productService.getProducts();
}
```
The `async` pipe automatically subscribes to the observable returned by the `getProducts` function and returns the latest value it has emitted. When a new value is emitted, it marks the component to be checked for changes, including ancestor components (**ProductShell** is one of them in this case). Now, Angular will check for changes **ProductShell** component together with its component tree including the **ProductList** component, and thus UI will be updated with products rendered on a table:

![Navigating from home page to the product's list page, we get the list of products rendered on the table.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zxstqks4cj67pbx92nmj.gif)

**Solution 2: Using Angular Signals 🚦**

`Signals`, introduced in Angular v16 in the developer preview, represent a new reactivity model that tells the Angular about which data the UI cares about, and when that data changes thus easily keeping UI and data changes in sync. Together with the future [**Signal-Based**](https://github.com/angular/angular/discussions/49682) components, will make possible fine-grained reactivity and change detection in Angular.
>  *You can read more about `Signals` in the [official documentation](https://angular.io/guide/signals).*

In its basics, a `signal` is a wrapper around a value that can notify interested consumers when that value changes. In this case, the ‘*products*’ data model will be a signal of the products which will be bound directly to the template and thus be tracked by Angular as that component’s dependency:

```
@Component({
  ...
  template: `
    <table>
      ...
      <tbody> <!-- getter function: read the signal value-->
        <tr *ngFor="let product of products()">
          <td>{{ product.title }}</td>
          <td>{{ product.description }}</td>
          <td>{{ product.price }}</td>
          <td>{{ product.brand }}</td>
          <td>{{ product.category }}</td>
        </tr>
      </tbody>
    </table>
  `
})
export default class ProductsListComponent implements OnInit {
  products = signal<Product[]>([]);
  productService = inject(ProductsService);

  ngOnInit() {
    this.productService.getProducts().subscribe((products) => {
      this.products.set(products);
    });
  }
}
```
When the '*products*' signal gets a new value (through the setter function), being read directly on the template (through a getter function), Angular detects changed bindings, marking the **ProductList** component and all its ancestors' components as dirty / for change on the next change detection cycle.

Then, Angular will check for changes **ProductShell** component together with its component tree including the **ProductList** component, and thus UI will be updated with products rendered on a table:


![Navigating from the home page to the product's list page, we get the list of products rendered on the table.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s9ruu37c8xvn58do0x9l.gif)

The same solution can be achieved by following a declarative approach using the `toSignal` function:

```
@Component({
  selector: 'app-products-list',
  standalone: true,
  imports: [NgFor, AsyncPipe],
  template: `
    <table>
      …
        <tr *ngFor="let product of products()">
          <td>{{ product.title }}</td>
          <td>{{ product.description }}</td>
          <td>{{ product.price }}</td>
          <td>{{ product.brand }}</td>
          <td>{{ product.category }}</td>
        </tr>
      …
    </table>
  `
})
export default class ProductsListComponent implements OnInit {
  productService = inject(ProductsService);
  products: Signal<Product[]> = toSignal(this.productService.getProducts(), {
    initialValue: [],
  });
}
```
`toSignal` is a utility function provided by [@angular/core.rxjs-interop](https://angular.io/api/core/rxjs-interop) (in developer preview) package to integrate signals with RxJs observables. It creates a signal which tracks the value of an Observable. It behaves similarly to the [`async`](https://angular.io/api/common/AsyncPipe) pipe in templates, it marks the **ProductList** component and all its ancestors for change / dirty thus UI will be updated accordingly.

You can find and play with the final code here: https://stackblitz.com/edit/onpush-cd-deep-route?file=src/main.ts 🎮

Special thanks to @kreuzerk,  @eneajaho, and @danielglejzner  for review.

**Thanks for reading!**

This is my first article, and I hope you enjoyed it 🙌.

For any questions or suggestions, feel free to leave a comment below 👇.

If this article is interesting and useful to you, and don’t want to miss future articles, give me a follow at [@lilbeqiri](https://twitter.com/lilbeqiri), [Medium](http://lilbeqiri.medium.com) or [dev.to](https://dev.to/ilirbeqirii). 📖