Angular Signals

Understanding Angular Signals in Simple Terms

Sasidharan SD
Stackademic

--

Angular developers have been using observables and special tools for a while to keep their apps up-to-date and responsive. There have been many different ways to do this. But with the release of Angular 16, the Angular team has introduced a new method called “Signals” to improve how Angular apps respond.

1. What are Signals?

Signals are a way for Angular to better understand and track how an app uses its data. This helps Angular update the app’s display more intelligently, making the app run smoother and faster. Signals are basically a method to manage the app’s data so that Angular can refresh the screen more effectively.

Think of a signal as a box that holds a piece of data. It alerts the app whenever this data changes. The data in a signal can range from simple to complex. Since the app always accesses this data through a signal, Angular knows exactly where and how it’s being used. Some signals can be changed, while others are just for looking.

Signals aren’t just for Angular; they’ve been used in other frameworks like SolidJS. The Angular team even worked with the creator of SolidJS to add signals to Angular.

2. Why do we need Signals?

I was initially puzzled about the need for Signals. I knew they made apps more responsive, but we already had Observables for that. Later, I learned Signals might eventually take over the role of Zone.js in Angular apps.

Zone.js is a part of Angular that decides when and which parts of an app should update. It’s not always the best at this, sometimes updating parts that don’t need it. Improvements here are especially helpful for larger apps.

Using Signals gives Angular a better idea of what parts of the app need updating, making the app more efficient and faster. As Signals become more widely used, Angular apps will likely see improved performance.

3. Creating Signal:

Let’s start by learning how to make a signal and use it in an app. First, you need to get the signal from @angular/core. Then, create a variable like this:

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

public favoriteColor: Signal = signal('blue');

Here, we’ve made a new signal and put it in the favoriteColor variable. We’ve set its starting value to “blue”. To show this signal’s value in the component, use interpolation as usual, but add parentheses to get the signal’s value:

<p>{{ favoriteColor() }}</p>

That’s pretty straightforward, right? But usually, we’ll want to change the signal’s value. So, how do we do that?

public changeColor(newColor: string) {
this.favoriteColor.set(newColor);
}

When you call this method, the signal’s value changes, and so does what’s shown on the screen. This is similar to changing a regular variable’s value, but it’s more efficient in complex and larger applications.

4. Updating Signal:

Signals can do more than just change a value. You can also change a signal’s value with the update method. The difference is that you use the signal’s current value to decide the new one. For example, imagine you’re counting how many times a button is pressed. Each time it’s pressed, you want to add one to the count. You can do it like this:

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

export class ButtonCounterComponent {
count: Signal = signal(0);

buttonPressed() {
this.count.update(current => current + 1);
}
}

Every time the button is pressed, the signal updates by adding one to the current count.

5.Mutating Signal:

The “mutate” method in Angular’s Signals works like “set” or “update” by changing the current value of the signal. The key difference is that it changes the value directly, right where it is. This means you can modify part of an object in the signal without having to replace the whole object.

Imagine you’re showing an item in your app, like a to-do list item, and you want to mark it as done. Your signal might hold an object with a title and a completion status.

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

export class TodoComponent {
public todoItem: Signal = signal({ title: 'Grocery Shopping', completed: false });

completeTask() {
this.todoItem.mutate(item => item.completed = true);
}
}

In this code, we’re using the “mutate” method to change just the ‘completed’ part of our todoItem. We pass a function to “mutate”, which allows us to directly change the ‘completed’ status to true. We don’t need to send a whole new object to the signal; just changing one part of the existing object is enough. This makes it easier and more efficient, especially when dealing with complex objects.

6. Computed Signals:

Computed signals in Angular are signals whose value is based on other signals. You create one using the computed function and defining how the value is derived:

const count = signal(0);
const doubleCount = computed(() => count() * 2);

ere, doubleCount depends on count. When count is updated, Angular knows it needs to update anything that relies on count or doubleCount.

Key Features of Computed Signals:

  1. Lazy Evaluation and Memoization: The value of doubleCount isn’t calculated until it’s first needed. Once calculated, this value is stored (cached), and future reads of doubleCount use this cached value without recalculating it.
  2. Invalidation and Recalculation: When count changes, it tells doubleCount that its cached value is out-of-date. However, doubleCount only recalculates its value the next time it's read.
  3. Not Writable: You can’t directly change the value of a computed signal. For instance, trying to set doubleCount directly will cause a compilation error since it’s not a WritableSignal.
  4. Dynamic Dependencies: Computed signals track only the signals actually used in the derivation function. For example:
const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`;
} else {
return 'Nothing to see here!';
}
});

In this case, if showCount is false, conditionalCount returns a message without using count. Thus, updates to count won’t affect conditionalCount. However, if showCount is set to true, conditionalCount will include count in its next calculation. Any change in count will then trigger a recalculation of conditionalCount. Dependencies can also be removed in the same way.

This dynamic nature allows for flexibility and efficiency, especially in cases of conditional logic or computationally intensive calculations.

7. Effects :

Effects in Angular’s Signals are operations that run whenever the values of certain signals change. You can create an effect using the effect function:

effect(() => {
console.log(`The current count is: ${count()}`);
});

Here’s what you need to know about effects:

  1. Initial Run and Dependency Tracking: Effects run at least once initially. They track which signal values they read, and if any of these signal values change, the effect runs again.
  2. Dynamic Dependency Tracking: Like computed signals, effects dynamically track their dependencies, focusing only on signals read in their most recent execution.
  3. Asynchronous Execution: Effects always execute asynchronously during Angular’s change detection process.
  4. Developer Preview Status: As of now, the effect() API is still in developer preview as part of integrating signal-based reactivity into Angular.

Use Cases for Effects:

  • Effects are great for logging data changes for analytics or debugging.
  • They can synchronize data with window.localStorage.
  • They’re useful for adding custom DOM behavior or interacting with third-party UI libraries like charting tools or canvas elements.

When Not to Use Effects:

  • Avoid using effects to propagate state changes, as this can lead to errors, infinite loops, or unnecessary change detection cycles.
  • Setting signals within effects is generally not allowed, but there are exceptions if really needed.

Creating and Managing Effects:

  • Typically, you register a new effect within a component, directive, or service constructor. This provides the necessary injection context.
  • You can also assign the effect to a field, which gives it a descriptive name and keeps it organized.
  • To create an effect outside of a constructor, pass an Injector to effect via its options.

Destroying Effects:

  • An effect is automatically destroyed when its enclosing context (like a component) is destroyed.
  • Effects provide an EffectRef which can be used to destroy them manually.
  • You can also create effects that last until manually destroyed, but remember to clean them up when they’re no longer needed.

Conclusion

In conclusion, Angular’s Signals and Effects offer a dynamic and efficient way to manage and respond to data changes in your applications. However, they need to be used judiciously to avoid performance issues. By understanding how to implement and utilize these features, you can significantly enhance the reactivity and performance of your Angular projects. Whether you’re synchronizing data, managing state changes, or simply reacting to user interactions, Signals and Effects provide a robust solution.

If you found this article helpful and would like to dive deeper into the world of Angular and modern web development, don’t forget to follow me for more insights and tips. Your support encourages the creation of more content like this, tailored to make complex concepts simple and approachable. Happy coding!

Medium: sasidharansd

Github: sasidharansd 👩‍💻

LinkedIn: sasidharansd 🏢

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--