First look at Signals in Angular

Welcome to your first glimpse into the reactive universe of Angular Signals. As part of Angular’s renaissance, this guide introduces powerful tools which help you build reactive signal-based applications.

According to the Angular documentation, a signal is a wrapper around a value that notifies interested consumers about changes. Signals can encapsulate any data type, ranging from simple primitives to complex data structures. Essentially, signals are functions that return their current value while notifying dependents of any changes.


Signals in Action

Below, you will find a demo application without signals and our goal will be to refactor it and apply the new reactive primitives.

visual of our amazing app
<p>Search Hero: {{search}}</p>

<input type="text" (input)="setSearchHero($event)">
<ul>
    <li *ngFor="let hero of filteredHeroes">{{hero.name}}</li>
</ul>

<button (click)="addHero()">Add Hero</button>
export class SearchHeroComponent {

 search = ''
 heroes = [
   {id: 1, name: 'Spider-Man'},
   {id: 2, name: 'Scarlet Witch'},
   {id: 3, name: 'Hulk'}
 ];

 filteredHeroes = this.heroes;
  
 setSearchHero(e: Event) {
    this.search = (e.target as HTMLInputElement).value
    this.filteredHeroes = this.heroes.filter(
      hero => hero.name.startsWith(this.search)
    )
  }
	
  addHero() { 
   this.heroes = [...this.heroes, {id:4, name: 'Iron Man'}]
  }
}

Absolutely, nothing special indeed! 😄 Now, let’s dive into the fun part of using the Signal API.


The Signals API offers several functions that are useful for our daily tasks. We categorize signals into two types: writable signals, which allow direct modification of their values, and computed signals, which derive their values from other signals and are read-only by nature.

Let’s see them in action one by one 😉

Writable Signals

How to Create a Writable Signal?

To get started, we need to import the signal function from the @angular/core package.

import { ..., signal } from '@angular/core';

Creating a signal itself is straightforward: we call the signal function with the initial value. Usually, the type of Signal and its value will be automatically inferred by TypeScript. I typed the search property just for demo purposes 😉

export class SearchHeroComponent {
 	search: WritableSignal<string> = signal('')
	...
}

As I mentioned earlier, you can use any data structure as a signal value so we can refactor our hero’s array to the signal as well.

heroes = signal([
  { id: 1, name: 'Spider-Man' },
  // ...
])

How to change the signal Value

When a user types a new value in the text input, the value of the search signal has to be updated accordingly. To update the value of this signal, we can use set(), which sets up a new value and notifies any dependent signals. Below, you can see how to do that.

setSearchHero(e: Event) {
    this.search.set((e.target as HTMLInputElement).value)
    //...
}

However, in certain situations, when you work with non-primitive data structures, we need to update or recalculate values based on their previous value (exactly the case with our heroes array). To address this, the Signals API provides an update() method.

update(): allows you to modify the value of the signal, forcing you to do it in an immutable way. For example, to add a new hero, we provide a callback function that takes a current heroes signal value as an argument and returns a new one based on the current heroes array.

To make sure that we did it immutably, we made a copy of the current heroes array using the spread operator [...hero] and eventually, add a new hero to the end of this new array.

addHero() {
  this.heroes.update(heroes => [...heroes, {id:3, name: 'Iron Man'}])
}

NOTE! This last step is very important because if you simply mutate the array using something like Array.push, then other dependent signals won’t be notified of this change.

Reading the signal value

Now, we can read value changes from the signal by unwrapping it, e.g., in the component template using parenthesis. It might remind you of something like Observable.subscribe() to read the value from the stream, which we covered in Streams Analogs In Real Life post.

<p>Search Hero: {{search()}}</p>

Read-only Signal

The last function in the Writable Signals API is asReadOnly(), which returns a read-only version of the signal. Read-only signals can be accessed to read their value but can’t be changed using set() or update() methods.

...
readonly _search = this.search.asReadonly()

In your template, we can bind to this read-only signal as a regular signal:

<p>Search Hero: {{_search()}}</p>

Note! It is important to note that asReadOnly() does not inherently prevent deep mutation of their value, e.g., if the value of the signal is an object, you can mutate its property (but it is not recommended).


Computed signals

Derived values from multiple signals

computed(): computes a new Signal, obtaining a reactive value through a combination of other signals. The compiler then interprets these signals to create a new derived value. This provides a powerful mechanism for creating dynamic and reactive values within your application.

filteredHeroes = computed(
  () => this.heroes().filter(
    hero => hero.name.startsWith(this._search())
  )
)

Here, the filteredHeroes property is a computed signal that dynamically filters the heroes based on the current search criteria. Whenever the heroes() signal or the search() changes, the computation is automatically re-executed to update filteredHeroes, ensuring that everything is reactively updated and adapts to changes seamlessly.

To display these filtered heroes, we leverage Angular’s newly redesigned control flow syntax, @for. This modern syntax offers a more concise, performant, type-safe, and readable way to iterate over collections.

<ul>
  @for (hero of filteredHeroes(); track hero.id) {
    <li>{{hero.name}}</li>
  }
</ul>

Signals and side effects

For scenarios where we wish to execute side effects in response to signal value changes, such as storing the search value in local storage for reuse upon page reload, we can employ the effect() function.

This adds a layer of flexibility and functionality by enabling additional behaviors or actions tied to signal value changes. Effects are always executed asynchronously during the change detection process.

logger = effect(() => {
    localStorage.setItem('searchHero', this._search())
})

effect(): Triggers operations whenever one or more signal values change, ensuring at least one execution. Such dynamic behavior guarantees real-time responsiveness to signal changes, facilitating the execution of side effects and keeping the application’s state synchronized with the data.

storing search results in local storage

As a final touch, let’s initialize our search signal with a value from the value we saved in the browser’s local storage or an empty string otherwise. We can simply modify the logic when we create the corresponding signal like that

search = signal(localStorage.getItem('searchHero') || '')

Conclusion

I hope 🤓 you will rewrite your enterprise applications with signals and remove all the boilerplate and clutter. Remember, using the right tool for the right job is key. Do not use Signals for everything. Signals are great for synchronous reactivity and state management. For asynchronous reactivity and events, RxJS remains the superior choice.

In case you are wondering 🤔  What about RxJs and turning your Observables and Subjects into Signals? Don’t worry. The Angular team has the answer to this question with the RxJS Interop API, which we will cover in the next blog post. Stay tuned and focus on Signals 🚦 while you are driving.

To play with the demo application I used for the article, you can check out this StackBlitz.

About the author

Tom Kotlar

Angular Developer

Add Comment

Tags

Tom Kotlar

Angular Developer

Get in touch

Decoded Frontend
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.