How to compute Angular signal from Observable?
Asked Answered
D

2

6

I have started to slowly embrace the new Angular's Signal in my projects. I stumbled upon the following scenario, and while it works, I wonder if this is the "Angular way" of doing it.

In my component, I have a signal which lacks of an initial value, so I initialise it like this:

private readonly _id: Signal<string | undefined> = signal(undefined);

Only on ngOnInit I'm really able to set the id (e.g. this._id.set('valid-id')); The thing is that I need the id in order to perform an Http request. In an Observable world, I'd go in this direction:

public readonly result$ = toObservable(this._id).pipe(
    filter(Boolean),
    switchMap((id) => this.myService.getById(id))
);

As a novice in the Signals world, I'd go like this:

public readonly result = toSignal(
    toObservable(this._id).pipe(
        filter(Boolean),
        switchMap((id) => this.myService.getById(id))
    ),
    { initialValue: [] }
);

But I'm afraid that maybe I'm a little biased towards my love for RxJS and Observables :)
So I wonder if there's an Angular way to do this instead? Perhaps I have to use compute() or effect()? Calling .subscribe() is definitely not an option!

Drue answered 25/2 at 19:2 Comment(3)
if result will be read in the template, your last code example, not - the first example. Use signals in the template, but for any reactivity that requires functional of Observables, it is still better to implement it using Observables.Oneiromancy
@OZ_ Yes, result would be consumed in the template. What if I want to display a loading indicator, how could that be integrated? I feel like my head is still full of RxJS operators and I'm tempted to use tap() to toggle an isLoading signal :)Drue
I think you can use either ngxtension.netlify.app/utilities/signals/computed-async or ngxtension.netlify.app/utilities/signals/computed-from. Both are about having a mix of signals & observables in a less boilerplate wayStallion
G
10

Signals aren't replacing observables. They're good to simplify computed, synchronous data.

As soon as it comes to async data, observables is the way to go.

Signals are new and help to make cascading synchronous updates, which is nice and simple. But it solves a completely different problem than RxJS who's focus is to solve async data flows.

So despite the new trend of trying to use signals everywhere, I'd argue that different tools are for different usages.

As an example, here's a very recent tweet I've seen:

enter image description here

This look very nice, simple and shiny. But it also introduces major race conditions that'll at best make your app look buggy and at worst make your app misbehave and potentially trigger side effects when they shouldn't or with a wrong value.

See my 2 responses where I explain a bit why here and here, but the race condition is gone with RxJS using one operator (switchMap, concatMap, exhaustMap, mergeMap based on what you want). So again, use the right tool for the job.

One could wonder what's the official recommended way. While it's not explicitly mentioned to use observables in the documentation for async data flow, you can see here that it is what they do and demo: https://angular.io/guide/rxjs-interop#toobservable

If you really wish to use a Signal instead of an Observable in your case while preserving a reactive flow that'll guarantee that you call getById whenever the _id changes, you should do exactly what you've done with toObservable and toSignal. But it's bothering you for a good reason: There's no real point in doing that here. Embrace Observables for async data flow if you don't want to have race conditions, and keep full control over how things happen and when. So in your case, unless you've shared a minimal example of what your code is doing and you really need this value to be transformed into a Signal so that it can be used with other signals, just use an observable.

Observables are complex when one has been thinking imperatively forever (which was my case a few years back). But they're not here for fun or make people who use them cool. They're here to give you the opportunity to manage more or less complex async data flows, which signals won't. The learning curve may be steep but the reward is worth it.

Gibbous answered 4/3 at 9:6 Comment(4)
Thank you for the answer! Quick follow-up question: What if I want to display a loading indicator, how could that be integrated with my current approach? I feel like my head is still full of RxJS operators and I'm tempted to use tap() to toggle an isLoading signal :)Drue
Shame, I've been searching for the past 15mn an answer I made where I did an in depth explanation of just that, but couldn't find it... I wonder if the question has been removedGibbous
I'm afraid that'd take more than a comment to answer properly. Feel free to open up a new question and share it here so I can find itGibbous
Aaaaaaaaaah found it. https://mcmap.net/q/1773490/-my-http-request-is-not-firing-anymore-after-i-started-pulling-data-in-from-a-reactive-form-observable-stream have a look at my answer and also the stackblitz I shared there. I think I implemented exactly what you're looking for (and without using a tap as side effect, all as a stream)Gibbous
F
-1

Kari, try this out. You might need effect, I guess.

import { JsonPipe } from '@angular/common';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import {
  Component,
  DestroyRef,
  OnInit,
  effect,
  inject,
  signal,
  untracked,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import 'zone.js';
import { filter, pipe, switchMap, tap, timer } from 'rxjs';
import { rxMethod } from '@ngrx/signals/rxjs-interop';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Hello from {{ name }}!</h1>
    <p>id: {{ id() }}</p>
    <p>data:</p>
    <pre>{{ data() | json }}<pre>
  `,
  imports: [JsonPipe],
})
export class App implements OnInit {
  name = 'Angular';
  data = signal<any>(void 0);
  id = signal<number | undefined>(void 0);
  url = (id: number) => `https://jsonplaceholder.typicode.com/todos/${id}`;
  http = inject(HttpClient);
  destroyRef = inject(DestroyRef);

  fetchData = rxMethod<number | undefined>(
    pipe(
      filter(Boolean),
      tap((value) => console.log(`piped ${value}`)),
      switchMap((id) => this.http.get(this.url(id))),
      tap((value) => console.log(`piped 2 ${value}`)),
      tap(this.data.set)
    )
  );

  constructor() {
    /**
     * rxMethod
     */
    this.fetchData(this.id);
    /**
     * an alternative with `effect`
     */
    // effect(() => {
    //   const id = this.id();
    //   if (!id) {
    //     return;
    //   }
    //   untracked(() =>
    //     this.http
    //       .get(this.url(id))
    //       .pipe(tap(this.data.set), takeUntilDestroyed(this.destroyRef))
    //       .subscribe()
    //   );
    // });
  }

  ngOnInit() {
    this.id.set(1);
    timer(2000)
      .pipe(
        tap(() => this.id.set(2)),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe();
  }
}

bootstrapApplication(App, { providers: [provideHttpClient()] });

Source code @ stackblitz

Freeboot answered 25/2 at 20:30 Comment(2)
Thanks! But I think your approach is way more complex than mine :)Drue
I don't see it as a way more complex :) This is what effect if for. If you want something that looks more declarative and reactive then your second approach is quite enough when you want only Angular features :) But if you want something from around Angular, you can utilize rxMethod from @ngrx/signals. I've modified my answer and the stackblitz so that you can see it in action :)Freeboot

© 2022 - 2024 — McMap. All rights reserved.