Photo by David Guenther on Unsplash

Angular Signals

Duncan Faulkner
ngconf
Published in
6 min readMay 31, 2023

--

I’ve started to look into Signals as this seems to be the hot topic of the day and I wanted to write a post about what I discovered and create a showcase application that (hopefully) explains Angular Signals that’s easy to understand.

Angular version 16 was released at the beginning of May and this version is jam-packed full of new features, but one feature everyone is talking about is the new Signals feature. This is a real game changer, currently in developer preview and set to be released later this year, in version 17.

What are Signals?

Signals are used for notifying interested parties when a value changes. A signal can contain any type of value, ranging from simple primitives (string, number, boolean) to intricate data structures (like an object or an array). To track where the signal is used, Angular always accesses the signal’s value through a getter function. Signals can either be read-only or writable. Writeable signals use an API to update their values, to create a writable signal call the signal function passing in an initial value.

Signals

Signals have the following methods:

  • Set — this is used to set a new value.
  • Update — this is used to update a previously set value.
  • Mutate — this is used to update complex objects.
  • Effect — this optional method is called when a value changes (see more details on effects below).

Declaring a Signal — we need to give the Signal an initial value.

//initialise a signal
const elementType = signal('animate');

To set a new value we can use the set method.

//set a new value
const elementType.set(value.elementType);

To update a signal with a previous value we use the update method.

//update a value
const elementType.update(value => value.elementType);

Mutate, when the Signal contains an object, rather than pushing a new object (which you could do) you can just update that object directly.

//mutate a value
const element = signal([{elementTitle:'animation', elementType:'Function'}]);
element.mutate(value => {value[0].elementType = 'Class'});

A computed signal obtains its value from other signals and is lazily evaluated and the value is cached. What this means is a computed signal won’t be calculated until the signal is read, once it has been read the value is cached. In the example below, until fullname is read it won’t be calculated, once it has then the result is cached and all subsequent reads will use the cached result. If firstname or lastname changes then fullname will be recalculated.

//compute a value
const firstname = signal('firstname');
const lastname = signal('lastname');
const fullname = computed(() => `${firstname()} ${lastname()}`);
console.log(fullname());

//output --> firstname lastname

When a signal value changes, an effect is triggered. An effect will run at least once and will keep track of signal reads, when the value changes the effect is triggered again. Effects can be useful in some circumstances for example:

  • Data needs logging to the console for debugging.
  • Keeping in sync with local/session storage.
  • Custom DOM behaviour.
  • Custom rendering (charts, canvas or other UI components).
effect(()=> {
console.log(`Fullname: ${this.fullname()}`);
}

Let’s start to build out our application and put some of these to work.

The application we will build (or at least part build in this post) will be a take on the Periodic Table Instead of using the elements though, we’re going to be using the Angular API.

As we’re using Angular 16 we can now create a new Standalone application from the command line (also new in v16), we won’t go through all the steps in this post to set up this project there will be a link to a GitHub repo at the end. To create a new Standalone from the command line type:

ng new <project-name> --standalone

We’ll create a Typescript file and populate it with an array of objects, I’ll just post part of it here to give you an idea.

export const mockApiData = [
{
elementType: 'F',
title: 'animate',
description:
'A multi-provider token that represents the array of registered HttpInterceptor objects.',
color: '#4caf50',
group: 'animations',
type: 'Function',
},
{
elementType: 'I',
title: 'AnimateChildOptions',
description:
'Adds duration options to control animation styling and timing for a child animation.',
color: '#009688',
group: 'animations',
type: 'Interface',
},
{
elementType: 'C',
title: 'AnimationDriver',
description: '',
color: '#2196F3',
group: 'animations/browser',
type: 'Class',
},
];

There are two parts to this component. The first part is the section that represents the element from the periodic table, but we’ll populate it with the mockApiData above.

<ng-container *ngFor="let api of mockData">
<div
class="shape panel element-type"
[style.background-color]="api.color"
(click)="loadDetails(url())"
(mouseenter)="mouseEnter(api)"
>
<div>
<div class="element-type">{{ api.elementType }}</div>
</div>
<div>
<div class="description">{{ api.title }}</div>
</div>
</div>
</ng-container>

The main point to notice in this code is the click event. Here we are using the Signals to pass the URL for the item that’s been selected to the event in the code file. The URL is being set initially when the page loads (see ts file code below). As we hover over a tile, the elementType is set to a new value. The click event then reads this new value and passes it to the event handler.

Let’s take a look at the code file to see what’s happening.

export class PodComponent {
// populate data from mockApiData
mockData = apiData;

// set up the initial Signals getting the first item in the array
elementType = signal(this.mockData[0].elementType);

title = signal(this.mockData[0].title);

description = signal(this.mockData[0].description);

color = signal(this.mockData[0].color);

group = signal(this.mockData[0].group);

// here we are computing the URL, reading the group and title signal
url = computed(() => `${API_URL}/${this.group()}/${this.title()}`);
}

As mentioned at the start of this post, Signals require a default value. To do this, we create a variable to store the value of the first elementType in the data array as a Signal and we do this for the title, description, colour, and group. The last variable (url) is slightly different: here we need to build up the URL that points to the Angular API documentation for the selected item. This is an excellent use of the computed function as we can get data from other signals, manipulate it and then return a new value. A computed Signal is not writable, you have to supply one or more signals and/or another value.

We can do something similar in the mouseEnter event, this time passing in the hovered-in item. Instead of using the set method again, this time we’ll use the update method.

mouseEnter(value: {
elementType: string;
title: string;
description: string;
color: string;
group: string;
}): void {
this.elementType.update(() => value.elementType);
this.title.update(() => value.title);
this.description.update(() => value.description);
this.color.update(() => value.color);
this.url = computed(() => `${API_URL}/${this.group()}/${this.title()}`);
}

In this application when we hover over an API we’re going to display another tile with some additional information and supply the URL to open the Angular documentation page for the selected API.

<div class="container">
<div class="shape element-type" [style.background-color]="color()">
<div>{{ elementType() }}</div>
</div>

<div>
<div>{{ title() }}</div>
<div>{{ description() }}</div>
<div>{{ url() }}</div>
<button mat-button (click)="loadDetails(url())">API Docs</button>
</div>
</div>

In the above code, we are now able to read the signals for elementType, title, description and the URL. We are also able to read the signal and pass this into the loadDetails click event. We can even read the signal value for the colour and in this example set the background colour of the tile.

This is just a small part of the application and I’m looking to expand on it over the coming weeks and to write more about this application.

This is my first look at Signals and I wasn’t sure at first what it would bring to the table, but the more I dig into it, the possibilities seem endless, as we can create services that use Signals to manage our state, we can convert them to and from observables, it seems nearly every day someone is able to find a new thing for Signals to do, so very much looking forward to learning more about this new feature.

The start of this application can be found here on stackblitz.com.

--

--