Asynchronous operations with (Angular) signals.

10min read

Featured for Asynchronous operations with (Angular) signals.

The Frontend Ecosystem lately

The world of frontend development is always buzzing with new ideas, tools, and frameworks. It’s like a giant playground where everyone’s trying out cool new toys. But slowly over the past years, something surprising has been happening: the authors behind these frameworks are starting to agree on some key ideas. Shocking, right?

For example many agree on:

  • — Server-side rendering (SSR) is a must

Many frameworks keep pushing the boundaries of what is possible with SSR. And since it boosts performance and SEO, it’s become the “must-have” feature.

  • — Components over templating

It’s all about components now! Forget templating, component based architecture is the new standard. Almost everyone’s on board. (Okay, maybe not everyone, but close enough. htmx logo)

  • — Web Components Are… Not Fun?

Let’s just say web components haven’t won the popularity contest. Most devs agree they’re tricky to use, so you’d better have a pretty convincing reason to pick them over regular frameworks.

carniato web components blog postrich harris web components blog post
  • — Signals everywhere

Signals ha’ve taken the community by storm, and for good reason! Like I mentioned in a previous post signals are becoming the new “must-have feature”. Ryan Carniato popularized the idea of them through SolidJS. Soon after many frameworks followed like Svelte5, Angular, Vue, Preact, Qwik.

Why do we need signals?

The short answer is: we don’t. Just like we don’t need a Virtual DOM, component-based architectures, or even an SPA framework to deliver a better UX. What signals offer is a fresh perspective: simplicity, readability, ease of use, and direct dependency tracking. They allow you to reduce complexity and manage reactivity in a more intuitive, straightforward way, and all of this natively!

Understanding signals.

Using the actual definition of signals from Angular in particular (although the underlying concept should be the same for the rest) we get:

signals definition

The key takeaways from the above is that signals:

  • — Help the framework to optimize rendering updates.

Signals ensure that whenever you access their values, you always get the latest values. Moreover computed signals are lazily evaluated. This also guarantees glitch-free UI synchronization.

  • Granularly track our state.

Signals operate at a fine-grained level, allowing you for more precise control over updates and reactivity.

All signals operations are synchronous!

Updating a signal happens synchronously, and any computed signal dependent on it will immediately reflect the change. Watchers are also notified synchronously as well.

Leveraging signals for asynchronous operations.

async signals Since we’ve established that all signal operations are synchronous, how can we leverage their benefits for asynchronous operations? There are some approaches and some best practices when using signals for that.

So we will see a small demo of an app and try to replicate it with different approaches and explain why some of them preferable and some are not.

Demo App

Our objective is to implement a lightweight asynchronous feature: fetching the astronomy picture of the day from NASA’s API for a specified date. When the date changes, it should trigger a new fetch, and any ongoing requests should be canceled to handle updates efficiently. demo nasa app NASA’s endpoint

Rxjs focused approach

First, let’s explore a more traditional RxJS-focused approach. By using a Subject combined with switchMap, we can initiate and cancel ongoing requests efficiently.

traditional-rxjs.ts
dateSubject = new BehaviorSubject<Date | undefined>(undefined);
pictureOfTheDayService = inject(PictureOfTheDayService);
pictureOfTheDay = signal<PictureOfTheDay | undefined>(undefined);
constructor() {
// I prefer to use a subject over `toObservable` because
// the latter is using an `effect` underneath.
this.dateSubject.pipe(
switchMap(date => this.pictureOfTheDayService.get(date)),
takeUntilDestroyed()
).subscribe(pictureData => this.pictureOfTheDay.set(pictureData))
// Using only signals to synchronize UI with component state--^
}

There couple things we can note here. Not only we take advantage of powerful RxJS operators but the key benefit here is that we maintain a unidirectional state pattern by seting our state at the end of our asynchronous flow. This means we have a signle state of truth that always remains immutable and only then we ensure UI consistency as well by leveraging the power of signals to seamlessly synchronize the UI.

However we have a small drawback here, we need to break our declarative paradigm since we need to subscribe, imperativly set our data, and handle the subscription itself.

Naive signals approach

In this case, our date signal triggers the effect, and inside the effect, we store the data using again the pictureOfTheDay signal.

naive-signals.ts
date = model<Date>();
pictureOfTheDayService = inject(PictureOfTheDayService);
pictureOfTheDay = signal<PictureOfTheDay | undefined>(undefined);
fetchPicture = effect(async () => {
const pictureData = await this.pictureOfTheDayService.getAsPromise(this.date());
this.pictureOfTheDay.set(pictureData); // I'm the trigger of this flow ---^
}, { allowSignalWrites: true }); // Not longer needed after Angular19

But our example above lacks the functionality of a switchMap like before. Also we are setting a signal within an effect block.

Seting a signal within an effect.

Seting a signal within an effect can create couple issues. Depending on the implementation it can create “glitches” since it is scheduled by the framework and the timing of some operations may not be so intuitive at first (will elaborate later), also it could produce an infinite loop:

infinite-fun.ts
myNum = signal<number>(0);
myDeadlyCycle = effect(() => {
console.log("My number is: ", this.myNum()); // I'm a depedancy of this effect
this.myNum.update(num => ++num); // I'm triggering my dependants
});

If you cannot avoid the above pattern for whatever reason you could utilize the untracked API provided by the Angular team.

end-infinite-fun.ts :(
myNum = signal<number>(0);
myNotSoDeadlyCycle = effect(() => {
untracked(() => {
console.log("My number is: ", this.myNum()); // I'm NO longer a depedancy of this effect
})
this.myNum.update(num => ++num); // I'm triggering my dependants
});

And while we could utilize untracked to make our previous example more readable and have more depedancy control, we would still lack the switchMap behavior.

Use of third party libs

We bridge this gap between our signals and Rxjs we could utilize third party libraries like: ngxtension. From that we could use derivedAsync API which brings the best of both worlds:

ngxtension-derived-async.ts
date = model<Date>();
pictureOfTheDayService = inject(PictureOfTheDayService);
pictureOfTheDay: Signal<PictureOfTheDay | undefined> =
derivedAsync(
() => this.pictureOfTheDayService.get(this.date()),
{ behavior: 'switch' } // this is the default behavior if not provided
);

Here derivedAsync makes the asynchronous call, provides us with back a Signal type, and it also manages to use the Rxjs operators. How does it achieve all of this?

Well if we check parts of its source code:

derived-async.ts
...
assertInjector(derivedAsync, options?.injector, () => {
...
effect(() => {
// we need to have an untracked() here because we don't want to register the sourceValue as a dependency
// otherwise, we would have an infinite loop.
// this is needed for previousValue feature to work
const currentValue = untracked(() => {
const currentSourceValue = sourceValue();
return currentSourceValue.kind === StateKind.Value
? currentSourceValue.value
: undefined;
});
...
untracked(() => {
sourceEvent$.next(
isObservable(newSource) || isPromise(newSource)
? newSource
: of(newSource as T),
);
});
});
...
}

Source code of derivedAsync

We notice the following:

  • — Uses the effect block to schedule the asynchronous operation.

  • — Forces the user to run the effect inside an injector context.

This is because the effect uses the DestroyRef service to unsubcribe.

  • — Makes use of the untracked API to ensure to avoid an infinite loop.
  • — Utilizes a Subject instead of a signal.

This is happening for two reasons. First of all it uses pipe() into the Rxjs operators, and most imporantly (although not the case here) its to ensure that all the events will be eagerly propageted to the system.

Native solution.

Thankfully now with Angular19 we do not need to use any external library to achieve this behavior not even Rxjs! Especially for newcomers I think this is a great selling point.

Resources object.

With Angular19 we also get the resources() API. Which although still experimental its proven very promising.

resources.ts
date = model<Date>();
pictureOfTheDayService = inject(PictureOfTheDayService);
pictureOfTheDayResource = resources({
request: this.date, // when I change I trigger the callback below.
loader: async ({ request: date }) => // this callback runs inside an effect!
await this.pictureOfTheDayService.getAsPromise(
date
),
})

With this approach all we need to do is pass a callback to the loader property and also give it the request property which basically tells the resource object to repond to changes from the provided signal.

Not only that but it will also cancel any on-going HTTP calls using an AbortSignal, so we can have the same functionality we had with a switchMap operator!

Creating a resource object will provide us with other usefull signals as well:

resource.ts
export interface Resource<T> {
readonly error: Signal<unknown>;
readonly isLoading: Signal<boolean>;
readonly status: Signal<ResourceStatus>;
readonly value: Signal<T | undefined>;
...
}

Linking signals.

Lets try now to expand our app by adding a gallery for favorite days. With this feature, you can save specific days to local storage and later view them, along with their descriptions, without needing to fetch them again.

demo nasa app bug

So our feature is working great, except from the fact that when we update our date() signal after we have selected a picture from our gallery doesn’t seem to show our newly fetched picture.

If we check our implementation it makes sense since in our template:

aod.html
<!-- We always try to render the selectedPicture() if it exists -->
@if (selectedPicture()); as selectedPicture) {
<app-picture-of-the-day (saveClick)="onSave($event)">
</app-picture-of-the-day>
} @else if (pictureOfTheDayResource.value()) {
<app-picture-of-the-day (saveClick)="onSave($event)">
</app-picture-of-the-day>
}

Well one obvious solution to our issue it would be to maybe reset our selectedPicture when our date changes.

avoid-this-effect-use.ts
resetEffect = effect(() => {
this.date(); // trigger the callback
this.selectedPicture.set(undefined);
})

And while the above works for our usecase, its a bad idea because like we mentioned already effect’s are scheduled which means that we have sometime between our date signal being set and our actual effect being scheduled to run. This can create glitches to the user, if not careful enough.

What would you do instead is this:

reactivly-linking-signals.ts
state = computed(() => ({
date: this.date(),
selectedPicture: signal<PictureOfTheDay | undefined>(undefined)
}));

we could shift our perspective and initialize a completely new signal directly when we receive a new value. This way, we eliminate the need for resetting and ensure the signal is updated reactively and consistently.

Alternativly you could use a new experimental API called linkedSignal:

linked-signals.ts
selectedPicture: WritableSignal<PictureOfTheDay | undefined> = linkedSignal({
source: this.date,
computation: () => undefined
});

✨ TLDR ✨

In conclusion its worth keeping in mind the following:

  • — ✅ Use signals if it alleviates complexity.
  • — ✅ Use signals for UI synchronization.
  • — ❌ Avoid signals for very complex asynchronous operations. (case dependant ofc)
  • — 👉 Signals when accessed are always providing the latest value.
  • — 👉 computed signals are lazily evaluated.
  • — 👉 Effects are primitive wrappers of watchers signals.
  • — 👉 Effects are always asynchronous and framework dependant.
  • — ❌ Do NOT use effect to synchronize the state or other signals.