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.
Discover how at OpenReplay.com.
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.
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.
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.
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
andreactive
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:
Aspect | Signals | Observables |
---|---|---|
Nature of Data | Handle synchronous data primarily, representing a value at a specific moment. | Able to manage asynchronous and synchronous data streams and gradually emit different values. |
Dependency Tracking | Maintain 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 Handling | It does not have any built-in error handling systems. | You can manage errors using catchError and other operators. |
Performance | Prioritized 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. |
API | The 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.
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.
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
- Signals in Angular.
- Signals in Solid.js.
- New proposed Javascript standard for signals.
- Signals in Preact.
- Ref and reactive functions in Vue.
- Svelte stores.
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.