Angular Signals – How to use them in a simple way?

What are Signals?

On May 3, 2023, Angular 16 was released, introducing the possibility to use Signals. Signals are another way to implement the reactive programming paradigm in Angular. For quite some time, RXJS has been available for this purpose, and it doesn’t seem to be disappearing from the Angular world anytime soon. Generally, the current consensus is that RXJS should be used for more complex problem scenarios since it is more powerful but also harder to grasp. However, for simpler use cases, which typically make up the majority of an application, Signals are the preferred choice.

How are Signals used?

To create a Signal, you only need the signal function:

const mySignal = signal(myValue);

The Signal created now contains a value (myValue), which can be accessed using ().

const myCurrentValueFromSignal = mySignal();

If you want to assign a new value to the Signal, you use the set(...) method:

mySignal.set(myNewValue);

Alternatively, you can use other mutation methods (mutate and update). The update method is suitable for cases where the new value depends on the old value (e.g., in a counter):

mySignal.update(oldValue => {
   const newValue = oldValue + 1;
   return newValue;
});

This is an extended form to illustrate what exactly happens. In a simplified version, you would write:

mySignal.update(value => value + 1);

The mutate method is intended for manipulating objects (note: in JavaScript, arrays are also objects):

myTodosSignal.mutate(todos => todos[7].done = true);

One particular aspect here is that the modified object (in this case, an array) does not need to be returned. Additionally, we are not replacing the entire object but changing one of its contents. Still, by calling the mutate method, we indicate that the object has changed, and any subsequent updates need to be triggered.

What part of this is now reactive?

Correct, until now we have simply written values into a signal, which were then changed and read. So far, the signal has only acted as a container for a value. This is, of course, not the primary purpose of signals. It becomes interesting when we now use the signal in our template:

<my-child-component [value]="mySignal()"></my-child-component>

Once again, we call our signal as a function to access the value of the signal and pass this value to a component. The difference from the previous chapter is that the value is not only unpacked once but every time the signal receives a new value. The re-rendering of my-child-component can, therefore, be very resource-efficient and will only be triggered when [value] receives a new value, and that only happens when mySignal gets a new value. What is also convenient is that you can directly access a field of an object within a signal:

<email-link [email]="myUserSignal().email"></email-link>

However, caution is advised here because often the content of the signal is initially set to undefined, which would result in an error when trying to access the email field. It is, therefore, recommended to always use this notation:

<email-link [email]="myUserSignal()?.email"></email-link>

computed

Suppose we want to calculate a new value based on a signal value; traditionally, we have the option to achieve this using a pipe:

<email-link [length]="myUserSignal() | getLength"></email-link>

However, it is, of course, rather cumbersome to create a custom pipe just for this purpose. That’s why with signals, there is a more elegant way:

const myLengthSignal = computed(() => {
   const userValue = myUserSignal();
   if (userValue == undefined) {
      return 0;
   }
   return userValue.email.length;
});

With computed(...), we now have created a new signal called myLengthSignal. In contrast to our mySignal from the previous chapter, this one is read-only. The methods set, mutate, and update are not available for myLengthSignal. Instead, myLengthSignal has another fantastic property: the signal will automatically update when its base (myUserSignal) gets updated! The signal mechanism identifies all signals that are called within the computed function (using ()), and it subscribes to these signals. In our case, it’s only myUserSignal, but it could be many different signals. Every time any of these signals receives a new value, the computed function will be re-executed, and the value of myLengthSignal will be updated.

effect

If we want to react to a signal without creating a new signal, we can use effects:

effect(() => {
   const userValue = myUserSignal();
   if (userValue == undefined) {
      return; // early return
   }

   console.log("new user:", userValue);
});

Effects essentially work the same way as computeds, with the difference that they produce a side effect outside of the signals scope. This side effect can be as harmless as a console output, or it could involve writing to a field on the component (in this case, don’t forget to use changeDetectorRef.markForCheck()).

What makes effects special and very helpful is that the effect function can be async:

effect(async () => {
   const userValue = myUserSignal();
   if (userValue == undefined) {
      return; // early return
   }


   const posts = await fetch(`my.url/postsByUser/${userValue.id}`);
   this.posts = posts;
   changeDetectorRef.markForCheck();
});

In the above example, every time the user changes (and is actually set), the user’s posts are fetched and assigned to the posts field of the component. To inform Angular that it should re-render the current component, we additionally call markForCheck().

This goes even more elegantly by setting the value of another signal within the effect itself, eliminating the need for markForCheck():

posts: Signal<Post[]>;

constructor() {
    effect(async () => {
        const userValue = myUserSignal();
        if (userValue == undefined) {
            return; // early return
        }

        const posts = await fetch(`my.url/postsByUser/${userValue.id}`);
        this.posts.set(posts);
    }, { allowSignalWrites: true });
}

In this example, we have to let the effect know that it can have an effect on other signals (which we then leverage with this.posts.set(...)). This is done via { allowSignalWrites: true }.

One might get the idea of achieving the same thing simply using a computed:

posts: Signal<Post[]> = computed(async () => {
    const userValue = myUserSignal();
    if (userValue == undefined) {
        return [];
    }

    return await fetch(`my.url/postsByUser/${userValue.id}`);
});

However, unfortunately, this approach won’t work as expected. The reason is that computed(async () => ...); returns a Signal<Promise<T>> instead of a Signal<T>. Personally, I hope that this limitation will be addressed and made possible in the future.

Stay tuned for the next blog post about Angular Signal where I will describe how our team uses Signals in particular and what gotchas we discovered while using it.

About the author

Domenic Helfenstein

2 comments

  • Thank you for providing such an informative explanation of Signals in Angular! Your breakdown of how Signals work, from creation to usage in templates, is incredibly helpful for developers looking to leverage this feature in their projects. I appreciate the clear examples and the distinction between Signals, computeds, and effects, along with their respective use cases.
    I’m looking forward to your next blog post where you’ll delve deeper into how Signals are utilized in real-world scenarios and any lessons learned along the way. Keep up the excellent work, and thank you for sharing your expertise with the community!

Recent Posts