Angular Signals Guide: Architectural Patterns & Best Practices

In Angular, signals (as part of Reactive Programming) are used to manage state and handle events in an application, often with the help of libraries like RxJS. However, with the introduction of Angular Signals (a new experimental feature), Angular introduces a different approach to managing reactivity in a more declarative and fine-grained manner.

What are Angular Signals?

Understanding Angular Signals Concept

Signals are used in Angular for state creation and management. A signal is a thin layer that encapsulates a value. Using Signal in Angular, we can construct reactive variables. Customers can read signals at any time; they don’t delay value. From simple to complicated, it may hold any value.
SetTimeout(), Promises, and other asynchronous elements should not be added to Angular signals since they are strictly synchronous. Signals can be read-only or writable. By employing signals to monitor every state of our application, we make it possible for Angular to quickly determine which areas of the display require updating.

The code this tutorial is available in this Github repository

Creating Signal in Angular

Signals are functions that allow us to obtain the updated value or update the value. Here’s how to create a signal:

Creating Signals in Angular Applications

Here we have created a signal and assigned value 50 to it.

Reading Signal

We can read a signal by calling it, as signals are functions, so you can call them as:

Reading Signals in Angular

On the browser’s console, you can see the value which is held by the variable.

Writable Signal

Angular knows when to update getCalculatedValue whenever we change a value. Here, the value is a writable signal while getCalculatedValue is readonly.

 constructor() {

    const writableSignalEx: WritableSignal<number> = signal(100);

    const getCalculatedValue: Signal<number> = computed(() => writableSignalEx() * 10);

    console.log('Writable Signals: ' + getCalculatedValue());

  }Code language: JavaScript (javascript)

Setting Value to Signal

We can set the value to a signal by the set method.

Setting Value to Angular Signals

Updating Signal Value

We can update signal values by calling the update method of the signal:

Updating Angular Signal Value

Angular keeps track of where signals are read and when they are updated using this method. The framework updates the DOM with new state and performs other tasks using this information. Reactivity is the capacity to react to shifting signal values across time.

Using Equality Functions in Signal

If the old and new values are the same, Signal does not check for new values when the equality function is used. Because it doesn’t cause any updates, the performance is cleaner.

Using Equality Function with Angular Signals

What is computed() Expression and When to Use

A computed signal is a signal that produces its value based on other signals.
Since they don’t write to signals, all computed() signals only return the result based on inputs; they don’t have any side effects. They become pure functions as a result. Computed() does not allow writing to signals, hence it makes them pure functions. It makes writing decorative code easier.
It’s important to note that computed signals are both lazy evaluated and memoized, meaning that they don’t update when you revisit components or refresh the page; instead, they provide a cached value of the same value. If the value is changed, it reads for the difference.

Using Computed Expression with Angular Signals

We should consider the following points while using computed():

  • The signal that is computed is read-only. Because computed() is continually listening to fresh results, avoid making changes there. There will be a compiler error if you attempt to change a computed signal.
  • Avoid using asynchronous calls in calculated() since it automatically updates the changes when signal reads change.
Hire Expert Angular Developers for Signal-Based Applications

Making Use of the Signal and Computing Within Your Components

Using Signals and Computed in Angular Components

Interaction with Templates

Signal data that is modified in your component class is used by templates.
Double curly brackets can be used in your templates to obtain the signal value. In the event that signal values change, templates automatically update the values.

  <p>Addition: {{getAddition()}}</p>Code language: HTML, XML (xml)

Angular continuously checks for updated values and displays them on templates. Angular can get updated values by calling the updateAddition() function.
Angular allows dynamic values to be bound into DOM attributes or properties.
Such as:

  <button [disabled]="getAddition() < 0" (click)="updateAddition()">Update Addition</button>Code language: HTML, XML (xml)

OR

    <ul [attr.role]="updpateAddtion() > 0">Code language: HTML, XML (xml)

Using Signal with Control Flow @if and @for

If and for blocks can be used to display condition-based DOM elements.

@if (getDetails()) {

   <p>Using get details information here</p>

  } @else {

   <p>No information available yet</p>

  }Code language: HTML, XML (xml)

What is effect() and When to Use

Any procedure that occurs whenever one or more signal values change is called an effect. You can use the effect function to generate an effect. Effect() will be required more frequently with programming that is more imperative. We ought to attempt declarative code. Effects operate in a reactive environment, and any code you invoke within an effect will also operate in a reactive environment.
Every effect runs at least once. Any signal value reads are tracked when an effect is running. The impact repeats if any of these signal values change. While the change detection procedure is underway, effects always run asynchronously.

While using effect(), we should keep in mind that:

  • It will be easier to read and identify incorrect behavior if our function is smaller.
  • In untracked(), add the remaining code after reading the signals.
  • Logging data being displayed and when it changes, either for analytics or as a debugging tool.
  • Adding custom DOM behavior that can’t be expressed with template syntax.

Using the `effect()` API, you can respond to signal changes. It requires a callback function that runs each time the value of the signal changes. For ex:

effect(() => {

      const min = this.getMin();

      const max = this.getMax();

      untracked(() => {

        if (max > min) {

          console.log(`Maximum value is: ${max}`);

        }

      });

    });Code language: JavaScript (javascript)

  You can also assign the effect to a field :

import { Component, computed, Input, Signal, signal, WritableSignal } from '@angular/core';

@Component({

    selector: 'app-signal-basics',

    templateUrl: './signal-basics.component.html',

    styleUrls: ['./signal-basics.component.scss'],

    standalone: true

})

export class SignalBasicsComponent {

  currentAddition = signal(50);

  firstInput = signal(5);

  secondInput = signal(50);

  getAddition = computed(() => this.firstInput() + this.secondInput());

  constructor() {

    const writableSignalEx: WritableSignal<number> = signal(100);

    const getCalculatedValue: Signal<number> = computed(() => writableSignalEx() * 10);

    console.log('Writable Signals: ' + getCalculatedValue());

  }

  ngOnInit() {

    console.log('Reading Signals: ' + this.currentAddition());

    this.currentAddition.set(100);

    console.log('Signals after set : ' + this.currentAddition());

    this.currentAddition.update(value => value + 900);

    console.log('Signals after update : ' + this.currentAddition());

  }

  updateAddition() {

    this.firstInput.set(Math.floor(Math.random() * 100) + 1);

    this.secondInput.set(Math.floor(Math.random() * 100) + 1);

  }

}

Code language: JavaScript (javascript)

Destroying effects:

Remember that we should take care to remove these effects when they are no longer needed. Effects are automatically deleted when components are destroyed. By default, based on the circumstances, Angular will automatically clean up the effect function. When you create an effect inside a component, for instance, Angular will remove it when the component is removed. This also applies to directives and other code.

Clean up effects manually: 

The onCleanup function allows you to register a callback that is triggered either after the effect is removed or before the effect’s subsequent run starts.

Using Effects with Angular Signals

Signals and Observables

Like signal towers sending messages, observables have emitters that emit values.
Two methods are available if you need to read a signal in Observable’s pipe():

  1. Using a join operator, you can transform Signal into Observable so that your Observable can respond to changes in Signal.
  2. If you only require the current value and do not wish to respond to Signal or its variations, you can either subscribe() or read Signal directly in your operators. You don’t need untracked() in this case because observable is not a reactive context.

The Best Practices to Adhere to When Utilizing Signals in Angular

Example 1: Showing/Hiding a DOM with Signals

Signal is a useful tool for displaying or hiding different parts of the DOM based on API responses. Here’s an example of how to accomplish this:

@Component({

      selector: 'get-details',

      template: `

        <div *ngIf="!isGetDetails()">

          // No information available yet

        </div>

        <div *ngIf="isGetDetails()">

          // Using get details information here

        </div>

      `,

    })

    export class GetDetailsComponent {

      getDetails: WritableSignal<boolean> = signal(false);

      ngOnInit() {

        this.getDetails();

      }

      getDetails() {

        this.myService.getDetailsAPI().subscribe(data) => {

          this.getDetails.set(true);

        });

      }

      isGetDetails() {

        return this.getDetails();

      }

    }Code language: JavaScript (javascript)

Example 2: Using Angular Signals to Count, Sort, and Filter Data

In this example, we have developed a writable signal called books. It is made up of various values, like the book’s title, price, and availability. In order to obtain the filtered books based on their availability, a computed() signal was then built. Additionally, we have arranged the books by price.

After that, Effects was introduced to record the modifications. The log will be printed inside the effects whenever we change the signal. The signal has been changed to update the logs.

After then, the signals were changed by including a new object and by making changes to the old object to cause the event.
Filtering, sorting, and counting data according to different circumstances are examples of more complicated scenarios that can be handled by combining signals, computed signals, and effects.

import { Component, computed, effect, signal, WritableSignal } from "@angular/core";
@Component({

  selector: 'app-signals-exercises',

  templateUrl: './signals-exercises.component.html',

  styleUrls: ['./signals-exercises.component.scss'],

  standalone: true

})

export class SignalsExercisesComponent {

  books: WritableSignal<any> = signal([

    { price: 1000, title: 'School Books', isAvailable: true },

    { price: 2000, title: 'Story Books', isAvailable: false },

    { price: 3000, title: 'Top Novels', isAvailable: false }

  ]);

  // Computed signal based on the isAvailable flag to rank and filter prices

  getAvailableBooksSortedByPrice = computed(() => {

    const filteredResult = this.books().filter((book: { isAvailable: any; }) => book.isAvailable);

    return filteredResult.sort((a: { price: number; }, b: { price: number; }) => a.price - b.price);

  });

  constructor() {

    // Effect to record the filtered books and the number of books left

    effect(() => {

      console.log(`Available Books: ` + this.getAvailableBooksSortedByPrice().length);

    });

    this.updateBookElements();

  }

  updateBookElements() {

    // Change the writable signals values.

    const updatingElement = this.books();

    updatingElement[2].isAvailable = true;

    this.books.set(updatingElement);

  }

}Code language: JavaScript (javascript)

Example 3: The Interaction Between Components Happens Reactively, Leveraging Angular’s Signals System

Signal Declaration:
counterSignal(0) creates a reactive signal with an initial value of 0.
Signals are reactive, and any component subscribed to the signal will automatically update when the signal’s value changes.

ChildOneComponent: 

The increaseSignalCount method increments the signal’s value by 1 whenever the button is clicked.

ChildTwoComponent:

The counterSignal() method is used to retrieve the current value of the signal and display it in the template. This component will automatically re-render when the signal’s value changes.

ParentComponent:

The signal is shared via input binding to both child components. When the signal is updated by ChildOneComponent, ChildTwoComponent will react to this change without the need for manual change detection or subscriptions.

This component will be the parent, containing the signal and passing it to children.

import { Component } from '@angular/core';

  import { signal } from '@angular/core/signals'// Import signals from Angular

  @Component({

    selector: 'app-root',

    template: `

      ...

<hr>

   <div class="child-section">

     <app-child-one [signal]="counterSignal"></app-child-one>

     <div class="child-divider"></div>

     <app-child-two [signal]="counterSignal"></app-child-two>

   </div>

    `

  })

  export class ParentComponent {

    // Creating a signal with an initial value

    counterSignal = signal(0);

  }Code language: JavaScript (javascript)

In the ParentComponent, we declare a signal that holds an integer value. The signal is shared with both child-one and child-two components via input bindings.

This component will modify the signal state by updating its value.

import { Component, Input } from '@angular/core';

  import { signal } from '@angular/core/signals'// Import signals

  @Component({

    selector: 'app-child-one',

    template: `

        <div class="child-1">

     <h3>Child 1: Modify Signal</h3>

     <button class="button1" (click)="increaseSignalCount()">Increase Signal</button>

     </div>

    `

  })

  export class ChildOneComponent {

    @Input() counterSignal: ReturnType<typeof signal>;  // Receive the signal from parent

    increaseSignalCount() {

      this.counterSignal.set(this.counterSignal() + 1);  // Increase signal value by 1

    }

  }Code language: JavaScript (javascript)

In ChildOneComponent, we receive the signal from the parent as an input. The increaseSignalCount() method modifies the signal’s state by increasing its value when the button is clicked.


This component will react to the changes in the signal and update itself accordingly.

import { Component, Input } from '@angular/core';

  import { signal } from '@angular/core/signals';

  @Component({

    selector: 'app-child-two',

    template: `

        <div class="child-2">

       <h3>Child 2: Current Signal Value</h3>

        <p>Signal Value: {{ counterSignal() }}</p>  <!-- Display signal value -->

        </div>

`

  })

  export class ChildTwoComponent {

    @Input() counterSignal: ReturnType<typeof signal>;  // Receive the signal from parent

  }Code language: HTML, XML (xml)

In ChildTwoComponent, we also receive the signal and directly use counterSignal() to retrieve its current value. The component will automatically re-render whenever the signal value changes.

Example of Angular Signal in Action

Summary

Signals have the capacity to improve the effectiveness of identifying changes and updating content, as well as to handle complications that were previously difficult, is its most intriguing aspect. The idea of signals is not new and exclusive to Angular. In various frameworks, often with other names, they have existed for years.
It is evident that the signal serves as a container for the value we wish to monitor. While the compute function is being called, Angular is monitoring and noting the usage of other signals that it is aware of.
Signals make it simple to monitor modifications to application data. Because of the simplicity of the Signals API and the signal notion, a codebase that employs signals everywhere would still be easy to understand.
Using signals in Angular applications can lead to more declarative, fine-grained reactivity, improving both performance and readability. By adhering to best practices such as maintaining a single source of truth, signal composition, and optimizing change detection, developers can harness the power of signals for better state management and UI interactions. Signals, especially when used in combination with Angular’s features like lazy loading and services, offer a modern approach to building efficient, scalable applications.

Improve Angular State Management with Signals

Author's Bio:

Author Bharat showcasing Angular SSR expertise
Bharat Kadam

Bharat Kadam has 8 years of experience as a senior software engineer at Mobisoft Infotech with a focus on Angular Frontend development. He is well-versed in Angular, React, Java and contemporary frontend/backend technologies. He is constantly keen to keep ahead of industry trends, is committed to producing clear, maintainable code and resolving challenging issues in the constantly changing frontend/backend environment.