Search code examples
angularfirebaseangularfirerxjs-observablesangular-signals

Using one signal for two observables that resolve at different times in Angular


I currently have four firebase functions set up, that perform queries in a MongoDB database. These functions are called fireSingleWord, fireAlphaList, fireSingleId and fireComplexWord.

Then my frontend application is in Angular 18, and I've set up a service where I declare four corresponding functions that returns an observable each. Like so...

  entrySingleArray$ = signal<Entry[]>([]);  
  private functions = inject(Functions);
  functionSingleWord = httpsCallable(this.functions, 'atlasSingleWord');

  fireSingleWord(lemma?: string): Observable<Entry[]> {
    return (
      lemma
        ? from(this.functionSingleWord(lemma)).pipe(
            map((x: HttpsCallableResult<unknown>) => x.data as Entry[]),
          )
        : of(this.emptySingleEntry)
    ).pipe(
      tap((response) => {
        this.entrySingleArray$.set(response);

Then in my components I subscribe to the observables like this, loading the function in the constructor:

  entry$ = {} as WritableSignal<Entry[]>;

  private fetchEntries() {
    effect(
      () => {
        this.entryService
          .fireSingleWord(this.searchService.mySearch().letter)
          .subscribe();
      },
      { allowSignalWrites: true },
    );
  }

Each of the functions is working independently, and they work fine. They bring the data from my MongoDB all the way to the frontend.

The problem: I have to make the functions work together, not independently, and often the result of one function provides the data to perform another query on a different function.

I have two components that are shown side by side and load at the same time. And I have three types of trigger events.

Event Component on the Left Component on the Right
Trigger1 fireAlphaList fireSingleWord
Trigger2 fireComplexWord fireSingleId
Trigger2 no changes fireSingleId

The arrows represent "I have to wait for this function to finish, to read the data obtained in the query result, in order to perform another query". For example, if Trigger1 happens, first I need to run fireSingleWord, and then with the result of this query I have to run fireAlphaList.

The problem I have is that I don't know how to make one function run only when another function has finished with these types of observables/subscriptions and I need the same two arrays entrySingleArray$ and entryListArray$ to reflect all changes on either component.

entryListArray$ = signal<Entry[][]>([]);  // for the component on the left
entrySingleArray$ = signal<Entry[]>([]);  // for the component on the right

So both fireAlphaList and fireComplexWord should impact on entryListArray$, and both fireSingleWord and fireSingleId should impact on the same object entrySingleArray$.

I've been going round in circles for a week. I've no idea how to go about this. Any help will be greatly appreciated.


Solution

  • The easiest method is to use computed, which triggers on each change on search.

    On the service, we create computed signals for each of the sequence of triggers. We can use switchMap which switches from the first API to the second API, all in one observable stream. This is generally used for nested APIs, where the second API depends on the first API details.

    export class SomeService {
        trigger1 = computed(() => {
            return this.entryService
              .fireSingleWord(this.searchService.mySearch().letter).pipe(
                  switchMap((res: any) => { // <- we can use res for the second API call
                      return this.entryService.fireAlphaList(res.something);
              )
        });
        // apply this same methodology for all the other triggers, we create the sequence of operations
        // at the service level, then we subscribe at the appropriate component level
    

    The service does not need to set any signals, so it will look like.

    fireSingleWord(lemma?: string): Observable<Entry[]> {
        return (
          lemma
            ? from(this.functionSingleWord(lemma)).pipe(
                map((x: HttpsCallableResult<unknown>) => x.data as Entry[]),
              )
            : of(this.emptySingleEntry)
        );
    }
    
    // transform the below function to look the same as above.
    
    fireAlphaList(lemma?: string) { ... }
    
    fireComplexWord(lemma?: string) { ... }
    
    fireSingleId(lemma?: string) { ... }
    

    Finally we go to using these computed signals. We can use this directly on the component like.

    <h1> {{ trigger1() | async | json }} </h1>
    

    Since the computed returns an observable we use the async pipe to subscribe to it.

    Or you can subscribe to the trigger and just get the data and set it to either the service signal or the component signal.

    ngOnInit() {
        this.entryService
          .fireSingleWord(this.searchService.mySearch().letter)
          .subscribe((res: any) => {
              this.entryService.entrySingleArray$.set(response);
        });
    }