Thought Leadership
Feb 24, 2023

I Built An App Using Angular’s Latest Prototype: Signals

And shared the code so you can too!
I Built An App Using Angular’s Latest Prototype: Signals

In this post we’ll create a small Rick & Morty app with the help of Signals and RxJS and will understand the role of each one.

You probably heard the news, Angular is prototyping with Signals and they are included in v16 pre-release. Of course you have read our previous article in collaboration with Edward Ezekiel about Signals and how it’s proposing fine-grained reactivity in Angular (you haven’t? I’ll wait here for you to read it).

Now lets build an app that uses Signals mixed with RxJS for handling asynchronous code.

tl;dr;

  • Signals are synchronous so they are not meant to replace RxJS but make it easier to implement reactivity in our apps with less code (no need to subscribe)
  • RxJS will stay, as we still need async reactivity, and interoperability between Observables and Signals is expected.

Signals, RxJS and Rick & Morty?

But before we start, you might ask, why mix Signals and RxJS, isn’t the main purpose of Signals to replace RxJS? Yes and no.

Signals are meant to be synchronous, and they are great for updating variables and results for the things we know or are happening at the moment, but not so great if we use them to fetch information from an API or to “wait” for an event to happen.

Why is this? Signals are not purely push like events or streams, nor purely pull like generators (A Hands-on Introduction to Fine-Grained Reactivity - DEV Community).

Angular’s implementation uses a push/pull algorithm:

“Dirtiness” (any difference between the previous and current state) is eagerly pushed through the graph when a source signal is changed, but recalculation is performed lazily, only when values are pulled by reading their signals.

This works great to simplify synchronous events like typing into an input field. But, this means that some asynchronous code isn’t handled well by Signals on their own. One good example, and one that we’re going to cover in this post, are race conditions.

Race conditions?

They are an undesirable situation that happens when two events run on parallel and they can finish on different time, affecting the end result of any given function or UI update. Take the following code:

We have a button that when clicked sets the signal value to an empty array and then calls an API with a random page (to have different loading times). What happens when the user clicks many times? Each click will send a request to the API and, based on how long it takes, we could not have the expected data on the page.

As we see, the request for page 5 finished after the last one for page 19. So, even if the last click was for page 19, we would see the values from page 5!

Oh no! A race condition!

Pictured: you, pair-programming, trying to handle all race conditions manually

We could disable the button when the user clicks it and enable it back when we get a response, but why create something new when we could use RxJS? By using already existing asynchronous methods, we could avoid race conditions and improve our code.

Let’s deep dive into our app

You can get the code from here: eduardoRoth/rick-and-morty-signals (github.com)

In this app I use Ionic for its great UI components, Nx (in standalone Angular app mode) for handling my repo, and Angular v16 to have Signals (developer preview) and we’re using standalone components.

You might need to use the legacy-peer-deps flag:

Here’s our app running:

We have a search bar where we can enter a name and the app will fetch from the API a list of characters that their name matches your input.

We’re using signals to handle the value updates from our search bar and an Observable (with the async pipe) to show our characters list.

But… how are we going to convert a signal to an Observable? Well, the Angular team is already working on interop functions, but for now we can create it!

What we’re doing here is creating a new Observable that we’ll return to the caller (so we can chain operator functions with the pipe method). Inside the constructor of the Observable, we’re calling the effect function so we can run code whenever the signal in the same scope as the effect function changes it’s value.

In short, we create a new observable that will emit every time a signal updates it’s own value.

My fromSignal is simple, but will suffice until we have an official function from the Angular team.

Let’s go to the code from the component (only showing what I’m going to be explaining).

Let’s look into the code:

We create two signals, searchQuery that will handle the string for the name that we’re searching with, and isLoading which will show or hide a loading spinner. charactersServices is the HttpClient get method that will fetch our characters.

characters$ is an Observable that is created from the searchQuery signal; so each time our signal is updated, it’ll update the observable too. Why aren’t we using subscribe() you ask? Well, I prefer to use the async pipe to let Angular handle component subscriptions and unsubscribes.

updateQuery is our method that will be called to the change event of our input and will update the searchQuery signal (this is synchronous).

Here’s the template code:

As you can see, we only use the async pipe once, and everything else is handled synchronously.

By using switchMap, RxJS automatically cancels the previous request, so we don’t have to worry about wrong information being shown (and also won’t finish unneeded requests).

Wait… but I can already do all of these with BehaviorSubjects/Subjects!

Yes, but I’m pretty sure you’ve ran into one of these:

  • Nothing being shown in the app if you forget to subscribe to the Observable, or if you are getting values from multiple observables and one of those didn’t emit (also nothing being shown)
  • A bunch of subscribes in your code and a subscription array to unsubscribe each BehaviorSubject/Observable on the ngOnDestroy event.
  • You don’t subscribe explicitly in your code, well, then you have a bunch of async pipe operators in your template.
  • And don’t forget about all the as variableName after the async pipe if you’re trying to use the value of the observable (which you’re actually going to do).
  • Not so intuitive *ngIf syntax (unless you use RxIf directive from RxAngular):
    *ngIf="characters$ | async as characters" and then below it *ngIf="characters.length > 0"
    Or, *ngIf="(characters$ | async).length > 0"
  • Calling toPromise (please, update to RxJS v7) or using the firstValueFrom or lastValueFrom functions to get the value of the observable to use it in your code.

Final thoughts

With the use of Signals and RxJS we will reduce the complexity for Angular apps, will improve the DX (won’t need to dirty check, monkey-patch APIs <bye Zone.js>, and worry about too many subscriptions), and will make apps faster as changes will be updated only where needed.

You can clone the repo and try it for yourself, or visit Home (rick-and-morty-signals.vercel.app) to try the app right now.

Update (2023–03–01)

After a discussion in a pull request regarding Signals and how we could avoid using the async pipe, Chau Tran pointed a way to remove it while using Signals and RxJS.

He proposed to use FormControl or a Behavior Subject to get the values from the ion-searchbar , and then use a prototype from the Angular team (with a small hack of his own) to create an Observable from a Signal.

The code has been merged and you can see the change in my repo.

Thanks Chau Tran!

. . .
Article Summary
Explore how to build a Rick & Morty app using Signals and RxJS in Angular 16. Learn to handle synchronous and asynchronous code seamlessly.
Author
Eduardo Roth
Software Engineer
Related Articles
HeroDevs Named Inaugural Partner for Drupal 7 Extended Security Support Provider Program
Ensuring Security and Compliance for Drupal 7 Beyond Its Official End-of-Life
HeroDevs Addresses Three CVEs in Unsupported Bootstrap
Addressing CVE-2024-6484, CVE-2024-6485, and CVE-2024-6531
Why HeroDevs Is Not Affected by the Polyfill.io Supply Chain Attack
Understanding HeroDevs' Immunity to the Polyfill.io Supply Chain Attack