Duplicate calls when using concatMap

831 Views Asked by At

I want to display in a view the next upcoming event a user is registered to.

To do so I need first to retrieve the closest event (in time) the user is registered to and then retrieve the information of this event.

Has the list of event a user is registered to is dynamic and so is the event information I need to use two Observable in a row.

So I tried to use concatMap but I can see that the getEvent function is called 11 times... I don't understand why and how I could do this better.

Here is my controller

//Controller
    nextEvent$: Observable<any>;

      constructor(public eventService: EventService) {
        console.log('HomePage constructor');
      }

      ngOnInit(): void {
        // Retrieve current user
        this.cuid = this.authService.getCurrentUserUid();
        this.nextEvent$ = this.eventService.getNextEventForUser(this.cuid);
      }

The EventService (which contains the getEvent function called 11 times)

// EventService
getEvent(id: string, company?: string): FirebaseObjectObservable<any> {
    let comp: string;
    company ? comp = company : comp = this.authService.getCurrentUserCompany();
    console.log('EventService#getEvent - Getting event ', id, ' of company ', comp);
    let path = `${comp}/events/${id}`;
    return this.af.object(path);
  }

  getNextEventForUser(uid: string): Observable<any> {
    let company = this.authService.getCurrentUserCompany();
    let path = `${company}/users/${uid}/events/joined`;
    let query = {
      orderByChild: 'timestampStarts',
      limitToFirst: 1
    };

    return this.af.list(path, { query: query }).concatMap(event => this.getEvent(event[0].id));
  }

And finally my view

<ion-card class="card-background-image">

    <div class="card-background-container">
      <ion-img src="sports-img/img-{{ (nextEvent$ | async)?.sport }}.jpg" width="100%" height="170px"></ion-img>
      <div class="card-title">{{ (nextEvent$ | async)?.title }}</div>
      <div class="card-subtitle">{{ (nextEvent$ | async)?.timestampStarts | date:'fullDate' }} - {{ (nextEvent$ | async)?.timestampStarts | date:'HH:mm' }}</div>
    </div>

    <ion-item>
      <img class="sport-icon" src="sports-icons/icon-{{(nextEvent$ | async)?.sport}}.png" item-left>
      <h2>{{(nextEvent$ | async)?.title}}</h2>
      <p>{{(nextEvent$ | async)?.sport | hashtag}}</p>
    </ion-item>

    <ion-item>
      <ion-icon name="navigate" isActive="false" item-left small></ion-icon>
      <h3>{{(nextEvent$ | async)?.location.main_text}}</h3>
      <h3>{{(nextEvent$ | async)?.location.secondary_text}}</h3>
    </ion-item>

    <ion-item>
      <ion-icon name="time" isActive="false" item-left small></ion-icon>
      <h3>{{(nextEvent$ | async)?.timestampStarts | date:'HH:mm'}} - {{(nextEvent$ | async)?.timestampEnds | date:'HH:mm'}}</h3>
    </ion-item>

  </ion-card>
2

There are 2 best solutions below

2
On BEST ANSWER

The this.af.list(path, { query: query }).concatMap(event => this.getEvent(event[0].id)) is a cold Observable. This means that each time you perform a subscription on it, it will re-execute the underlying stream, which means re-calling the getEvent method.

async implicitly subscribes to the Observable, which is why if you count up (nextEvent$ | async) calls in your template, you will see where the 11 comes from.

&tldr; You need to share the subscription to the stream:

this.nextEvent$ = this.eventService.getNextEventForUser(this.cuid)
   // This shares the underlying subscription between subscribers
   .share();

The above will connect the stream the first time it is subscribed to but will then subsequently share that subscription between all of the subscribers.

0
On

RXJS's concatMap method flattens all the events into one observable. You are probably better off using the switchMap method. switchMap subscribes only to the most recent observable.

So doing something like this would probably solve your problem:

Before:

return this.af.list(path, { query: query }).concatMap(event => this.getEvent(event[0].id));

After:

return this.af.list(path, { query: query }).switchMap(event => event);