1

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>
Manuel RODRIGUEZ
  • 2,131
  • 3
  • 25
  • 53

2 Answers2

2

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.

paulpdaniels
  • 18,395
  • 2
  • 51
  • 55
  • Thanks paulpdaniels! I actually noticed the 11 async pipes but was afraid that it would just be a coincidence. What is the purpose of these cold Observable? Would the switchMap avoid to share the subscription? I would like to avoid going through a too complex implementation for the moment. – Manuel RODRIGUEZ May 31 '17 at 21:25
  • 1
    @ManuelRODRIGUEZ no `switchMap` wouldn't fix the issue. The idea of cold `Observables` is functional purity, essentially that each time you perform an operation with a set of arguments you should expect to receive the same answer. Obviously in the real world that isn't always realistic, hence why operators like `share` exist, to convert cold `Observables` into hot ones, for scenarios just like this. – paulpdaniels Jun 01 '17 at 03:27
0

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);

Ty Sabs
  • 209
  • 1
  • 2
  • 10