Back

Understanding Signals in Angular

Understanding Signals in Angular

Signals allow developers to create dynamic and highly responsive applications by monitoring state changes and adjusting the user interface dynamically. This article describes signals in Angular, their operation, primary distinctions from observables, and leveraging them for state management at the global level and form handling. Additionally, it explores how other frameworks utilize signals before discussing the prospective JavaScript standard intended to formalize this idea.

Signals are reactive primitives used for state change management and tracking in applications. They enable the design of such dynamic, responsive UIs that can update various areas based on underlying state changes.

To understand signals, we must understand these three reactive primitives: writable signals, computed signals, and effects.

Writable signals

These specific types of signals in Angular allow you to modify their values directly. To demonstrate this, we will create a simple counter application. In this application, clicking the increment button increases the count by 1.

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, CommonModule],
  template: `
 <div>
 <div>{{ count() }}</div>
 <button (click)="increase()">Increment</button>
 </div>
 `,
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  title = 'signals';
  count = signal(1);

  increase() {
    this.count.set(this.count() + 1);
 }
}

In the code above, we created a signal count and initialized it to 1. Then, we defined an increase() method, which uses the set method to modify the signal’s value by setting it to the current value plus one.

The image below shows that the count increases when clicking the Increment button.

effect

Computed signals

These are values derived from other signals. They are automatically updated when the signals they depend on change. Computed signals are read-only, which means we cannot set the values of computed signals directly.

Here is an example of a computed signal. In the code below, we created three writable signal functions, length, height, and width, and initialized them. Using a computed signal, we calculate the volume by multiplying the three values defined earlier.

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, CommonModule],
  template: `<div>{{ volume() }}</div>`,
  styleUrls: ['./app.component.css'], 
})
export class AppComponent {
  title = 'signals';

  length = signal(12);
  height = signal(15);
  width = signal(12);

  volume = computed(() => this.length() * this.width() * this.height());
}

As seen in the image, the value of the volume is computed and displayed on the screen.

image

Effects

These are functions that run automatically when one or more signal values change. They always run at least once. It is important to note that you should not update any signals inside an effect. This is because it may trigger infinite circular updates and unnecessary change detection cycles.

Below is an example of an effect. A good place to create an effect is inside a constructor because the effect function requires an injection context.

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, CommonModule],
  template: `<button (click)="increase()">Effect button</button>`,
  styleUrls: ['./app.component.css'], 
})
export class AppComponent {
  title = 'signals';

  count = signal(1);

  constructor() {
    effect(() => {
      console.log('Effect triggered due to count signal:', this.count());
 });
 }

  increase() {
    this.count.update((value) => value + 1);
 }
}

In the code above, we defined a button that, when clicked, increments the count value by 1. An effect was set up to log a message to the console every time the count signal was updated.

The image shows that each time the button is clicked, the message ‘Effect triggered due to count signal’ is logged to the console.

effect (1)

How signals enable fine-grained reactivity

Signals enable fine-grained reactivity by establishing a direct and granular relationship between data and its dependents. Signals are more precise than traditional reactive systems, which use broad change detection methods. Once created, a signal retains a value while keeping track of the components or calculations dependent upon it. It enables the system to effectively update only the areas of the program that are directly impacted by a signal change, as opposed to reevaluating huge components or the entire application. This is due to dependency tracking, which is crucial for fine-grained reactivity.

This selective re-evaluation significantly improves performance by minimizing redundant calculations and renderings. Signals also support atomic updates, processing multiple changes as a single unit to prevent inconsistencies and performance bottlenecks. Moreover, signals can be composed to manage complex state interactions, enabling the creation of reusable reactive components. Signals breaking down reactivity into smaller, more manageable units provide a flexible and efficient foundation for building highly responsive and performant applications.

Signals in Other Frameworks

Signals are a key factor in several modern reactive frameworks, each with a different implementation to effectively manage state and reactivity. Below is an overview of how some popular frameworks use signals.

  • Solid.js: Among the top frameworks that utilize signals as the basis for its reactive system is Solid.js. As a result, Solid.js can achieve fine granular reactivity with high efficiency. The createSignal function is used to create signals in Solid.js, and it returns a getter and setter for controlling the signal value. The result is an extremely fast UI with less avoidable re-renders.

  • React (via external libraries): Although React does not have an inbuilt signal system, packages like @preact/signals provide React with access to a signal-based reactivity model. With these tools, developers can create signals in a React-like environment with better granularity levels and more effective state management methods. These libraries’ signals function similarly to Solid.js’, where modifications to signals cause dependant components to be re-rendered automatically, negating the need for manual dependency management.

  • Vue.js: Vue.js, based on proxies for its reactive system, primarily uses a Composition API with a concept of signals. The reactive state in Vue.js can be created by utilizing the ref and reactive functions, somewhat analogous to signals, whereby changes made to these reactive variables cause corresponding changes within the DOM. Although not called signals, the underlying principle of dependency tracking and targeted reactivity is similar.

  • Svelte: Svelte takes a different approach toward reactivity; it relies on a compiler that generates efficient update code. Similarly, signals share many characteristics with Svelte’s stores since they are reactive storage systems that notify any subscribed components of an update. Moreover, just like signals provide fine-grained responses, only those components making use of this specific data will be re-rendered when the store’s value changes.

The New Proposed JavaScript Standard for Signals

The suggested JavaScript standard regarding signals aims at making them an inherent part of the language. If implemented, it would offer all frameworks a common low-level API that developers can use directly without having to rely on external libraries. APIs would, therefore, be a part of such a standard that provides signals with an automated updating system; monitoring dependencies and triggering them whenever necessary can make reactive applications easier to create with fine controlling abilities.

The web development paradigm would change enormously with the inception of a common signal API, rendering specific frameworks’ reaction systems obsolete and making development more efficient. Browser engines could better understand how to handle signal-based updates, thereby making web applications responsive. Large-scale applications that depend highly on performance would especially appreciate this facility.

Though still in the early stages, there has been much enthusiasm and conversation about it in the JavaScript community. For further information about the proposal, visit here.

Key Differences Between Signals and Observables

Signals and Observables, while both mechanisms for managing reactive data have distinct characteristics:

AspectSignalsObservables
Nature of DataHandle synchronous data primarily, representing a value at a specific moment.Able to manage asynchronous and synchronous data streams and gradually emit different values.
Dependency TrackingMaintain dependencies automatically so that modifications can be made quickly when a signal’s value changes.Require explicit subscription management, allowing subscribers to express interest in value changes.
Error HandlingIt does not have any built-in error handling systems.You can manage errors using catchError and other operators.
PerformancePrioritized for their performance because they are based on synchronous data and effective tracking of dependencies.More performance costs could arise due to subscription maintenance and asynchronous operations.
APIThe API is more simplified, focusing on creating, updating, and reading values.Has a complex API that has various operators for merging, transforming, and modifying data streams.

Form Handling with Signals

Signals can be used to efficiently manage form state and validation by tracking the form’s input values and validation status. By using signals, you can automatically update the UI in response to changes in the form state without the need for manual change detection.

Let’s look at this example below:

export class AppComponent {
  title = 'signals';

  name = signal('');
  email = signal('');
  formSubmitted = signal(false);

  // Computed signals for validation
  nameError = computed(() => (this.name() === '' ? 'Name is required' : ''));
  emailError = computed(() =>
 !this.isValidEmail(this.email()) ? 'Invalid email' : ''
 );
  formValid = computed(() => this.name() && this.isValidEmail(this.email()));

  // Effect to handle form submission
  submitForm() {
    if (this.formValid()) {
      // Submit form data
      console.log('Form submitted:', {
        name: this.name(),
        email: this.email(),
 });
      this.formSubmitted.set(true);
 }
 }

  isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
 }
}

In this example, we created 3 writable signals for name, email, and formSubmitted. The first two signals are initialized to empty strings that will store the values inputted for name and email while the formSubmitted is initialized to false and it tracks if the form is submitted or not.

Next, we used computed signals for validation. It checks if the name field is empty and if it is, it returns an error message “Name is required”. We also check if it is a valid email format and if the submitted form is valid.

Finally, we define a submitForm method that evaluates the formValid computed signal, which checks if the form is valid, and if it is, it logs the data inputted to the console.

In the image below, we see that the form is validated in real-time. It also shows the data that was inputted when the form was submitted.

form (1)

Signal-based Service for Global State

By using signals within services, it is feasible to keep and update this state across diverse components. Whenever there is a change in the state, it may be emitted, allowing various components to listen for these changes and act accordingly. This ensures that every part of the application remains in sync with the current situation or status.

To create a global user state management signal-based service, first generate a new service using the command below:

ng generate service <service-name>

Then, create a writable signal _user that holds either an object with the respective user’s name and email or null.

Next, create a computed signal called _loggedIn, which depends on the _user. If the _user is logged in, true will be returned; otherwise, false will be returned. It will also include getters to make those signals accessible from other parts of the app.

Finally, use the set function to update the _user with the provided data or null.

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

@Injectable({
  providedIn: 'root', // Ensures the service is a singleton and available throughout the app
})
export class AppStateService {
  // Define signals for global state
  private _user = signal<{ name: string; email: string } | null>(null);
  private _loggedIn = computed(() => this._user() !== null);

  // Expose signals via getters
  get user() {
    return this._user;
 }

  get loggedIn() {
    return this._loggedIn;
 }

  // Method to update the user
  setUser(user: { name: string; email: string }) {
    this._user.set(user);
 }

  // Method to log out
  logout() {
    this._user.set(null);
 }
}

After setting up the signal-based service, you can inject this service into your components or other services to manage and access the global user state.

Next, create a login method that triggers the setUser method in the AppStateService. Then we pass an object representing the name and email properties.

export class AppComponent {
  title = 'signals';

  constructor(public appState: AppStateService) {}

  login() {
    this.appState.setUser({
      name: 'John Doe',
      email: 'john@example.com',
 });
 }
}

The image below shows the UI updated with the logged-in details when the login button is clicked.

effect (2)

Conclusion

Signals in Angular represent a significant step forward in building reactive applications with fine-grained control. As signals gain traction, the way developers perceive reactivity changes within Angular and other frameworks. Developers looking to make next-generation dynamic user interfaces must know how to leverage signals.

Additional Resources

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay