Logo

dev-resources.site

for different kinds of informations.

Angular Computed Signal with an Observable

Published at
2/18/2024
Categories
angular
signals
rxjs
webdev
Author
cyrillbrito
Categories
4 categories in total
angular
open
signals
open
rxjs
open
webdev
open
Author
11 person written this
cyrillbrito
open
Angular Computed Signal with an Observable

Angular signals provide a robust reactivity system, streamlining the data flow within Angular applications and also improving the developer experience.
However, their integration with RxJS can sometimes present challenges, leading to messy and less straightforward code.

In this blog post, we'll explore a strategy to streamline the interaction between Angular signals and RxJS, making their combined usage more straightforward and efficient.

Scenario - Pokédex

In an attempt to show some of the problems I created this simple scenario where we have a simple Pokémon display, and we use an API to get the Pokémon information. We can click on the Previous and the Next to iterate over the Pokédex, and the Refresh should re-fetch the information from the API.

print from the app

There are only two components, the AppComponent that contains the buttons, and the PokemonComponent that displays the information and makes the API calls.

The PokemonComponent has an id signal input and if it changes the component makes a new API call to get the Pokémon info. There is also the refresh() that must re-call the API for the current id.



@Component({
  selector: 'app-root',
  template: `
    <button (click)="id=id-1">Previous</button>
    <button (click)="pokemonComp.refresh()">Refresh</button>
    <button (click)="id=id+1">Next</button>
    <app-pokemon #pokemonComp [id]="id"/>
  `
})
export class AppComponent {
  id = 1;
}

@Component({
  selector: 'app-pokemon',
  template: `
    <h1>{{ pokemon()?.name }}</h1>
    <img [src]="pokemon()?.sprites?.front_default">
  `,
})
export class PokemonComponent {
  http = inject(HttpClient);

  id = input.required<number>();

  id$ = toObservable(this.id);
  refresh$ = new BehaviorSubject(null);
  pokemon$ = combineLatest([this.id$, this.refresh$]).pipe(
    switchMap(([id]) => this.http.get(`https://pokeapi.co/api/v2/pokemon/${id}`)),
  );

  pokemon = toSignal(this.pokemon$);

  refresh() {
    this.refresh$.next(null);
  }
}


Enter fullscreen mode Exit fullscreen mode

How to improve - computedAsync

The code above works but there is a lot of code gymnastic needed to make sure the Signal world and the RxJs world work together, from the toObservable, toSignal, and the combileLatest, and refresh$ it all feels a bit too clunky.

If we forget the observables and focus on what we are trying to do, we want a kind of computed that could unwrap the observable and store its value in the signal.



// Receives a function that must return an observable
function computedAsync<T>(computation: () => Observable<T>): Signal<T> {

  // Creates a signal that where the observable result will be stored
  const sig = signal<T>(null);

  // Uses effect to make sure that if there are changes in the computation
  // that it gets re-runned
  effect(() => {
    // Reset signal value
    sig.set(null);
    // Get the observable
    const observable = computation();
    // Subscribe and store the result in the signal
    observable.subscribe((result) => sig.set(result));
  }, { allowSignalWrites: true });

  return sig;
}


Enter fullscreen mode Exit fullscreen mode

With this, we already gain the amazing power of working the RxJs while depending on signals, this will simplify the code a lot as we will see soon.
But there are a few problems that we still need to solve, first, we need to find a way to replicate the refresh functionality, and second, we need to make sure to not leave open observable subscriptions.



function computedAsync<T>(computation: () => Observable<T>): Signal<T> & { recompute: () => void } {

  const sig = signal<T>(null);

  // Save current subscription to be able to unsubscribe 
  let subscription: Subscription;

  // Create an arrow function that contains the signal updating logic
  const recompute = () => {
    sig.set(null);
    // Before making the new subscription, unsub from the previous one
    if (subscription && !subscription.closed) {
      subscription.unsubscribe();
    }
    const observable = computation();
    subscription = observable.subscribe((result) => sig.set(result));
  };

  effect(recompute, { allowSignalWrites: true });

  // Add the recompute function to the returned signal, so that it can be called from the outside
  return Object.assign(sig, { recompute });
}


Enter fullscreen mode Exit fullscreen mode

Let's put it to use

Let's see how our PokemonComponent looks if we use the computedAsync.



@Component({
  selector: 'app-pokemon',
  template: `
    <h1>{{ pokemon()?.name }}</h1>
    <img [src]="pokemon()?.sprites?.front_default">
  `,
})
export class PokemonComponent {
  http = inject(HttpClient);

  id = input.required<number>();

  pokemon = computedAsync(() => this.http.get(`https://pokeapi.co/api/v2/pokemon/${this.id()}`));

  refresh() {
    this.pokemon.recompute();
  }
}


Enter fullscreen mode Exit fullscreen mode

I don't know about you but this feels like a huge improvement to the original code, there are no gymnastics to make sure the observables are triggered at the right time and no boilerplate toObservable and toSignal.

I think that the signals API is one of the best things that Angular added, it is very powerful and flexible, and with the deeper integration in the framework with will for sure get better. Let me know our thoughts about this in the comments. 🙂

signals Article's
30 articles in total
Favicon
New in Angular: Bridging RxJS and Signals with toSignal!
Favicon
A Complete Solution for Receiving Signals with Built-in Http Service in Strategy
Favicon
Vanilla JS Signal implementation
Favicon
How I'm Using Signals to Make My React App Simpler
Favicon
Angular Migrating to Signals: A Paradigm Shift in Change Detection
Favicon
The Problems with Signals: A Tale of Power and Responsibility
Favicon
Angular Signals: From Zero to Hero
Favicon
Mutable Derivations in Reactivity
Favicon
Introducing Brisa: Full-stack Web Platform Framework 🔥
Favicon
Async Derivations in Reactivity
Favicon
Scheduling Derivations in Reactivity
Favicon
Exploring Angular's Change Detection: In-Depth Analysis
Favicon
Understanding Reactive Contexts in Angular 18
Favicon
New Free eBook: Angular Mastery: From Principles To Practice.
Favicon
What's new in Angular 18
Favicon
Using @HostBinding with Signals
Favicon
Angular Inputs and Single Source of Truth
Favicon
Angular Signal Queries with the viewChild() and contentChild() Functions
Favicon
Converting Observables to Signals in Angular
Favicon
Angular Signals: Best Practices
Favicon
Streamlining Communication: New Signals API in Angular 17.3
Favicon
Signal-Based Inputs and the Output Function
Favicon
What's new in Angular 17.3
Favicon
Master Angular 17.1 and 17.2
Favicon
Angular Computed Signal with an Observable
Favicon
Django Signals mastery
Favicon
How to mock NgRx Signal Stores for unit tests and Storybook Play interaction tests (both manually and automatically)
Favicon
Derivations in Reactivity
Favicon
Improve data service connectivity in Signal Stores using the withDataService Custom Store Feature
Favicon
How Signals Can Boost Your Angular Performance

Featured ones: