DEV Community

Cover image for Signals in Angular – How to Write More Reactive Code
Omama Aslam
Omama Aslam

Posted on

Signals in Angular – How to Write More Reactive Code

An exciting new feature is coming to Angular: signals! Signals provide a new way for our code to tell our templates (and other code) that our data has changed. This improves Angular's change detection, which also improves performance, and makes our code more reactive.

You can try out this powerful new feature now. Signals are available for developer preview in Angular v16, due to be released in May of 2023. You can get early versions of Angular v16 to try it out now. I'll walk through how later in this tutorial.

Before we jump into the details of the "what?" and "how?", let's start with the "why?". Why would you want to use this new signals feature?

Why Do We Need Signals?

Let's start with a simple example without the use of signals. Say you are writing code to perform some basic math operations.

let x = 5;
let y = 3;
let z = x + y;
console.log(z);
Enter fullscreen mode Exit fullscreen mode

What does this code log to the console? Yep, it logs out 8.

Some time later in the code, we change the value of x. What does z log out now?

let x = 5;
let y = 3;
let z = x + y;
console.log(z);

x = 10;
console.log(z);
Enter fullscreen mode Exit fullscreen mode

It still logs out 8! That's because a value is assigned to z when the expression is first evaluated. The z variable does not react to changes in x or y.

But we want our variables to react to changes!

One of the reasons we use Angular is to build reactive websites, like Figure 1. When the user updates the quantity, the related variables (such as subtotal and tax) should react and adjust the costs. If the user selects to delete an item from the cart, we again want the related variables to react and correctly recalculate the costs.

Angular Signal Example

With signals, our code can be more reactive. Our prior example implemented with signals would look like this:

const x = signal(5);
const y = signal(3);
const z = computed(() => x() + y());
console.log(z()); // 8

x.set(10);
console.log(z()); // 13
Enter fullscreen mode Exit fullscreen mode

We'll look at this syntax in detail shortly. For now, the code above defines two signals: x and y and gives them initial values of 5 and 3. We then define a computed signal, z, which is the sum of x and y. Since signals provide change notifications, when the x or y signals change, any values computed from those signals will automatically recalculate. This code is now reactive! Nice!

Computed signals react and recalculate when any of its dependent signals change. If a signal is bound in a template, when the signal changes, Angular's change detection automatically updates any view that reads the signal. And the user sees the changed value.

So the answer to "why do we need signals?":

Signals provide more reactivity
Using signals gives us finer control over change detection, which can improve performance.

Let's dive a bit deeper into what a signal is and how it is used.

What Is a Signal?

You can think of a signal as a value plus a change notification. A signal is just a special type of variable that holds a value. But unlike other variables, a signal also provides notification when the variable value changes.

Think of a normal variable as a shelf, like on the left side of Figure 2. When a value is assigned to the variable, it sits on that shelf. Any code within scope can simply read that variable on the shelf.

Signal in Angular

A signal is more like a box, as shown on the right side of Figure 2. Creating a signal metaphorically creates a box and puts the value inside of that box. The box glows when the value of the signal changes. To read the signal, first open the box using parentheses: x(). Technically speaking, we call the signal's getter function to read the signal.

We now have the answer to "what is a signal?":

A signal is a variable + change notification
A signal is reactive, and is called a "reactive primitive"
A signal always has a value
A signal is synchronous
A signal is not a replacement for RxJS and Observables for asynchronous operations, such as http.get

Where can we use signals?

  1. Use them in components to track local component state
  2. Use them in directives
  3. Use them in a service to share state across components
  4. Read them in a template to display signal values
  5. Or use them anywhere else in your code

Next let's walk through how to create and use signals.

How to Create a Signal

To use a signal, you first create one.

quantity = signal<number>(1);
Enter fullscreen mode Exit fullscreen mode

The above syntax creates and initializes a signal using the signal constructor function.

Optionally, provide a generic type parameter to define the signal's data type. A signal can be a string, number, array, object, or any data type. In many cases, the data type can be inferred and the generic type parameter is unnecessary.

Pass to the constructor the default value of the signal. A signal always has a value, starting with that default.

Here are some additional examples:

quantity = signal(1);

qtyAvailable = signal([1, 2, 3, 4, 5, 6]);

selectedVehicle = signal<Vehicle>({ 
  id: 1,
  name: 'AT-AT', 
  price: 19416.13
});

vehicles = signal<Vehicle[]>([]);
Enter fullscreen mode Exit fullscreen mode

The first code line above creates a numeric signal with a default value of 1. Because the default value is a number, the quantity is a signal that holds a number. The generic type parameter isn't needed.

The second line is a signal that holds an array of numbers. The default provides an array of values 1 through 6. Again, the generic type parameter isn't needed in this case because it can be inferred from the default value.

The selectedVehicle signal holds a Vehicle object. In this example, the type cannot be inferred, so we specify a generic type parameter of Vehicle.

The vehicles signal holds an array of Vehicle objects. Its default is an empty array. To strongly type the array, we add a generic type parameter of .

A signal created with the signal constructor function is writable, so you can set it to a new value, update it based on the current value, or mutate its content. We'll see examples of these operations shortly.

Once you've created a signal, you may want to read its value.

How to Read a Signal

Earlier, we represented a signal as a box. Metaphorically speaking, to read a signal's value you must first open the box. You do that by adding parentheses as shown below.

quantity();
Enter fullscreen mode Exit fullscreen mode

Start with the signal name and follow it with open and closing parentheses. Technically speaking, this calls the signal's getter function. The getter function is created behind the scenes – you won't see it in your code.

When working with Angular, a common place to read signals is in the template.

<select
    [ngModel]="quantity()"
    (change)="onQuantitySelected($any($event.target).value)">
  <option *ngFor="let q of qtyAvailable()">{{ q }}</option>
</select>

<div>Vehicle: {{ selectedVehicle().name }}</div>
<div>Price: {{ selectedVehicle().price }}</div>
<div [style.color]="color()">Total: {{ totalPrice() }}</div>
Enter fullscreen mode Exit fullscreen mode

The above template displays a select box for selection of a quantity. The [ngModel] reads the value of the quantity signal, binding to that value.

The change event binding calls the onQuantitySelected() method in the component.

The option element uses ngFor to iterate through each array element in the qtyAvailable signal. It reads the signal and creates a select option for each array element.

Below the select element are three div elements. The first one reads the selectedVehicle signal, then accesses its name property. The second div element reads the selectedVehicle signal, then displays its price property. The last div element reads the totalPrice signal (which we have not yet defined). And it sets the text color to the value from the color signal (which we also have not defined).

It's important to note that reading a signal always reads the current signal value. The code doesn't have any knowledge of any prior signal values.

When the user picks a different quantity from the select element, we want to change the value of the quantity signal. That way the quantity signal becomes the "source of truth" for the user's selected quantity. Let's look at how to do that next.

How to Change the Value of a Signal

The signal set method replaces the value of a signal with a new value. It basically opens the box, removes the current item, and sets in a new item to take its place.

this.quantity.set(qty);
Enter fullscreen mode Exit fullscreen mode

A common scenario is to change the signal value based on a user action. For example:

  1. The user selects a new quantity using the select element
  2. The select element event binding calls the onQuantitySelected() method and passes in the selected quantity
  3. The user action is handled in that event handler within the component
  4. The new value is set into the quantity signal.

Here is an example event handler:

onQuantitySelected(qty: number) {
  this.quantity.set(qty);
}
Enter fullscreen mode Exit fullscreen mode

Whenever the signal is set, the code notifies any consumers that the signal has changed. In this context, a consumer is any code that is interested in receiving change notifications.

How does the consumer indicate that it's interested in receiving notifications about a particular signal?

How does the consumer indicate that it's interested in receiving notifications about a particular signal?

So the act of reading a signal registers the consumer's interest in watching that signal. The Angular team calls this the golden rule of signal components: "change detection for a component will be scheduled when and only when a signal read in the template notifies Angular that it has changed."

Here is an example to illustrate the process. Let's say that there is some work going on within the method below that needs to adjust the quantity. Maybe if the quantity is 5 or more you get one free, for example. The point is that the quantity signal could change several times within the execution of the method.

onQuantitySelected(qty: number) {
  this.quantity.set(qty);

  this.quantity.set(5);
  this.quantity.set(42);
}
Enter fullscreen mode Exit fullscreen mode

The quantity is displayed in the template using Angular's binding as shown below. Since the binding reads the quantity signal, the template registers its interest in receiving change notifications.

{{ quantity() }}
Enter fullscreen mode Exit fullscreen mode

When the user selects a quantity, the onQuantitySelected() method executes. The code in the method first sets the signal to the user-selected quantity. When the new signal is set, the signal generates a notification. At this point, Angular's change detection is scheduled to run. But it doesn't have an opportunity to run until after the execution of the onQuantitySelected() method.

The onQuantitySelected() method continues, setting the signal to 5. The signal generates another change notification. Again Angular's change detection is reminded that it needs to run, but it still can't run because the onQuantitySelected() method is still executing. The method then sets the signal to 42 and the process repeats.

When the onQuantitySelected() method has completed its execution, Angular's change detection can finally run. The template reads the signal, and gets the current value of that signal, which is 42. The template is not aware of any of the prior signal values. The view is then re-rendered, and the new quantity signal value is displayed.

If you are familiar with RxJS and Observables, signals are quite different. Signals don't emit values like Observables do. And signals don't require a subscription.

In addition to the set(), there are two other ways to change a signal: update() and mutate().

The set() method replaces a signal with a new value, metaphorically replacing the contents of the signal box. Pass the new value into the set method.

// Replace the value
this.quantity.set(qty);
Enter fullscreen mode Exit fullscreen mode

The update() method updates the signal based on its current value. Pass to the update method an arrow function. The arrow function provides the current signal value so you can update it as needed. In the code below, the quantity is doubled.

// Update value based on current value
this.quantity.update(qty => qty * 2);
Enter fullscreen mode Exit fullscreen mode

The mutate() method modifies the content of a signal value, not the signal value itself. Use it with arrays to modify array elements, and objects to modify object properties. In the code below, a vehicle's price is increased by 20%.

this.selectedVehicle.mutate(v => v.price = v.price + (v.price * .20));
Enter fullscreen mode Exit fullscreen mode

How to Define a Computed Signal

Oftentimes we have variables in our code that depend on other variables. For example, the total price for an item is the price for that item times the desired quantity of that item. If the user changes the quantity, we want to change the total price. For that, we use computed signals.

totalPrice = computed(() => this.selectedVehicle().price * this.quantity());

color = computed(() => this.totalPrice() > 50000 ? 'green' : 'blue');

The first line of code above defines a totalPrice computed signal by calling the computed() creation function. The computation function passed into this computed function reads the selectedVehicle and quantity signals. If either signal changes, this computed signal is notified and will update when it is its turn to execute.

The second line of code defines a color computed signal. It sets the color to green or blue depending on the value of the totalPrice signal. The template can bind to this signal to display the appropriate style.

A computed signal is read only. It cannot be modified with set(), update() or mutate().

The value of a computed signal is re-computed when:

1. One or more of its dependent signals is changed.
2. AND the value of the computed signal is read.


## How to Use an Effect
There may be times that you need to run code when a signal changes, and that code has side effects. By side effects I mean code that calls an API or performs another operation not related to the signal. In these cases, you'll use an effect().

For example, you want to debug your signals and log out the signal value each time the code reacts to a change to that signal. Calling console.log() is a side effect.

To define an effect, call the effect() creation function. Pass to the function the operation to perform. This operation is re-executed every time the code reacts to a change in any dependent signal.

Enter fullscreen mode Exit fullscreen mode

effect(() => console.log(this.selectedVehicle()));


The effect() function can be called within other function. Since the effect sets up a handler of sorts, it is often called in the constructor or other startup code.

Alternatively, an effect can be defined declaratively as shown below:

Enter fullscreen mode Exit fullscreen mode

e = effect(() => console.log(this.selectedVehicle()));



An effect should not change the value of any signals. If you need to change a signal based on a change to a dependent signal, use a computed signal instead.


**If you find this post helpful please dont forget to like, and share with your collegues also subscribe so you can get more posts that ehance your knowledge..... Thanks for watching**
Enter fullscreen mode Exit fullscreen mode

Top comments (0)