DEV Community

Nikhil Sai
Nikhil Sai

Posted on

Angular Signals : A Comprehensive Introduction

Angular as we know is a popular framework for building web applications that are dynamic, interactive, and responsive. One of the key aspects of Angular is its change detection mechanism, which is responsible for updating the view whenever the data changes. Angular uses a zone-based approach to detect changes, which means that it runs a change detection cycle whenever an event occurs, such as a user input, a timer, an HTTP request, etc.

However, this approach has some drawbacks, such as:

  • It can be inefficient, as it may run change detection for the entire application even if only a small part of it has changed.
  • It can be inconsistent, as some events may not trigger change detection automatically, such as web sockets or third-party libraries.
  • To address these issues, Angular introduces a new feature called signals, which is a new reactive primitive type that can notify interested consumers when its value changes. Signals can contain any value, from simple primitives to complex data structures. A signal’s value is always read through a getter function, which allows Angular to track where the signal is used.

Signals can be either writable or read-only. Writable signals provide an API for updating their values directly or indirectly. Read-only signals derive their values from other signals using a derivation function.

Signals provide several benefits, such as:

  • They are more efficient, as they only update the parts of the application that depend on them.
  • They are simpler, as they use a declarative syntax that is easy to read and write.
  • They are more consistent, as they react to any changes in their dependencies automatically.
  • In this article, we will explore how to use signals in Angular and see some examples of how they can improve our code. We will also learn about two other features of signals: compute and effect. Compute is a way to create computed values that depend on signals. Effect is a way to create side effects that run when signals change.

Creating Signals

To create a writable signal, we can use the signal function and pass the initial value of the signal as an argument. For example:


const count = signal(0); // Creates a writable signal with an initial value of 0
Enter fullscreen mode Exit fullscreen mode

To read the value of a signal, we can simply call it as a getter function. For example:

console.log(count()); // Logs 0
Enter fullscreen mode Exit fullscreen mode

To update the value of a writable signal, we can use the set method and pass the new value as an argument. For example:

count.set(1); // Sets the value of count to 1
console.log(count()); // Logs 1
Enter fullscreen mode Exit fullscreen mode

We can also use the update method to compute a new value from the previous one using a callback function. For example:

count.update(value => value + 1); // Increments the value of count by 1
console.log(count()); // Logs 2
Enter fullscreen mode Exit fullscreen mode

To create a read-only signal, we can use the computed function and pass a derivation function that returns the value of the signal based on other signals. For example:

const doubleCount = computed(() => count() * 2); // Creates a read-only signal that depends on count

console.log(doubleCount()); // Logs 4
Enter fullscreen mode Exit fullscreen mode

The derivation function of a computed signal is lazily evaluated and memoized, which means that it only runs when the signal is first read or when any of its dependencies change. The result of the derivation function is cached and returned for subsequent reads until it becomes invalid.

Using Signals in Templates

One of the main use cases of signals is to bind them in templates and let Angular’s change detection update the view automatically whenever the signals change. To do this, we need to use signals in our components and expose them as public properties.

For example, let’s say we have a simple component that displays a counter and allows us to increment or decrement it using buttons. We can use signals to implement this component as follows:

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

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <button (click)="decrement()">-</button>
      <span>{{ count() }}</span>
      <button (click)="increment()">+</button>
    </div>
  `,
})
export class CounterComponent {
  // Creates a writable signal with an initial value of 0
  count = signal(0);

  // Increments the count by 1
  increment() {
    this.count.update(value => value + 1);
  }

  // Decrements the count by 1
  decrement() {
    this.count.update(value => value - 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

In the template, we can use the count signal as a normal property and interpolate it using the {{ }} syntax. We can also call the increment and decrement methods on the component instance using the (click) event binding.

When we run this component, we can see that the view reflects the value of the count signal and updates whenever we click on the buttons. This is because Angular tracks the count signal as a dependency of the component and marks it for change detection whenever it changes.

We can also use signals in other template expressions, such as property bindings, attribute bindings, class bindings, style bindings, etc. For example, we can use a computed signal to disable the decrement button when the count is zero:

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

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <button (click)="decrement()" [disabled]="isZero()">-</button>
      <span>{{ count() }}</span>
      <button (click)="increment()">+</button>
    </div>
  `,
})
export class CounterComponent {
  // Creates a writable signal with an initial value of 0
  count = signal(0);

  // Creates a read-only signal that depends on count
  isZero = computed(() => this.count() === 0);

  // Increments the count by 1
  increment() {
    this.count.update(value => value + 1);
  }

  // Decrements the count by 1
  decrement() {
    this.count.update(value => value - 1);
  }
}

Enter fullscreen mode Exit fullscreen mode

In this case, we use the [disabled] property binding to bind the isZero signal to the disabled attribute of the button element. The isZero signal is a computed signal that returns true if the count signal is equal to zero, and false otherwise.

When we run this component, we can see that the decrement button is disabled when the count is zero and enabled otherwise. This is because Angular tracks the isZero signal as a dependency of the component and updates its view whenever it changes.

Compute and Effect

Another feature of signals is that they can be used to create computed values and effects. Computed values are signals that derive their values from other signals using a derivation function. Effects are operations that run whenever one or more signals change. Effects do not return any value, but they can perform side effects such as logging, updating local storage, calling APIs, etc.

To create a computed value, we can use the computed function and pass a derivation function that returns the value of the signal based on other signals. For example:

const name = signal('Alice'); // Creates a writable signal with an initial string value
const greeting = computed(() => 'Hello, ' + name()); // Creates a read-only signal that depends on name

console.log(greeting()); // Logs 'Hello, Alice'
Enter fullscreen mode Exit fullscreen mode

The derivation function of a computed value is similar to that of a computed signal, except that it does not need to wrap its return value in a getter function. The computed value will automatically update whenever any of its dependencies change.

To create an effect, we can use the effect function and pass an effect function that performs some operation based on one or more signals. For example:

const name = signal('Alice'); // Creates a writable signal with an initial string value
const greeting = computed(() => 'Hello, ' + name()); // Creates a read-only signal that depends on name

effect(() => console.log(greeting())); // Creates an effect that logs greeting

name.set('Bob'); // Updates name to 'Bob'
Enter fullscreen mode Exit fullscreen mode

The effect function of an effect is similar to that of a computed value, except that it does not return any value. The effect will automatically run whenever any of its dependencies change. The effect will also run at least once when it is created.

We can use compute and effect to create various kinds of reactive logic in our applications. For example, we can use compute to create a filtered list of todos based on a search query:

const todos = signal([
  { title: 'Learn signals', done: true },
  { title: 'Write an article', done: false },
  { title: 'Read a book', done: false },
]); // Creates a writable signal with an initial array value

const query = signal(''); // Creates a writable signal with an initial string value

const filteredTodos = computed(() => {
  // Creates a read-only signal that depends on todos and query
  return todos().filter(todo =>
    todo.title.toLowerCase().includes(query().toLowerCase())
  );
});
Enter fullscreen mode Exit fullscreen mode

In this case, we use the todos signal to store the list of todos and the query signal to store the search query. We use the filteredTodos signal to derive a new list of todos that match the query using the filter method. The filteredTodos signal will update whenever the todos or the query signals change.

We can use effect to create a local storage sync for our todos:

const todos = signal([
  { title: 'Learn signals', done: true },
  { title: 'Write an article', done: false },
  { title: 'Read a book', done: false },
]); // Creates a writable signal with an initial array value

effect(() => {
  // Creates an effect that depends on todos
  localStorage.setItem('todos', JSON.stringify(todos()));
}); // Syncs the todos to local storage whenever they change

effect(() => {
  // Creates an effect that runs once
  const storedTodos = localStorage.getItem('todos');
  if (storedTodos) {
    todos.set(JSON.parse(storedTodos));
  }
}); // Loads the todos from local storage when the app starts
Enter fullscreen mode Exit fullscreen mode

In this case, we use the first effect to sync the todos signal to local storage using the setItem method. The effect will run whenever the todos signal changes. We use the second effect to load the todos signal from local storage using the getItem method. The effect will run only once when the app starts.

Conclusion

In this article, we have learned about signals, a new feature in Angular that allows us to write reactive code using a simple and declarative syntax. We have seen how to create and use signals in our components and templates, how to create computed values and effects with signals.

Signals are a powerful and expressive way to manage data flow and state changes in our applications. They can help us write code that is more efficient, simpler, and consistent. They can also enable us to create more dynamic, interactive, and responsive user interfaces.

Thank's for your time, see you soon in another one :)

Top comments (1)

Collapse
 
rezaimn profile image
Mohammadreza Imani

RSM Signal State Management: A Powerful Alternative to NgRx

Hi, everyone, I have created a new angular library and wrote this article about it. It's a new state management based on Signals and it's much easier than Ngrx, lightweight and needs less boilerplate. please check it out and comment your opinion. thanks.

medium.com/@mohammadrezaimn/rsm-si...