SwitchMap operator with an array

2020-07-27 03:45发布

问题:

I'm trying to learn rxjs and the Observable concept in general and have a scenario where I have a class of <Room>{} where <Player>{} can join in many-to-many relationship style.

In Firestore I have collection of rooms where each room has a property called players which is an array of user uids.

When a rooms component is created I subscribe to _roomService.getPlayersInRoom(roomId) which looks like below:

getPlayersInRoom(roomId: string) {
  return this._db.doc<Room>(`rooms/${roomId}`).valueChanges().pipe(
    map(room => room.players),
    switchMap(players => players),//2
    switchMap(playerId => {
      if(playerId) {
        return this._db.doc<Player>(`users/${playerId}`).valueChanges();
      }
    })
  );
}

I subscribe to it later with

.subscribe(player => {
    if (player) {
      this.players = new Array();
      this.players.push(player);
    }

There are couple of issues here. My observable does not return an array of players as expected (see line //2 which transforms the string[] into a string)

Another issues is that I new up the this.players array in my component every time that room changes (otherwise the .push() will push in duplicates.

I've read documentation on some of these operators and I understand them somewhat but not enough to figure out why this code is not behaving the way it should.

回答1:

First, switchMap expects you to return an Observable. If its an array-like value — it will turn it into an array using from (see this from example).

If you really want to get an array back on the stream — you should return a stream, e.g. using of: switchMap(value => of([]))

Yet, in your case, you want to substitute each id in the array with a stream. We'll need to use a combineLatest operator, for example (the name speaks for itself). Each array of players we will switch to a new stream. This new stream we'll combine of latest values on valueChanges() streams.

Heres an example:

getPlayersInRoom(roomId: string) {
    return this._db.doc<Room>(`rooms/${roomId}`).valueChanges().pipe(
      map(room => room.players),
      switchMap(players => {
        // turn array of player IDs
        // into array of streams of changes
        const playersStreams = players.map(
          playerId => this._db.doc<Player>(`users/${playerId}`).valueChanges()
        );

        // combine latest changes from all streams in that array
        return combineLatest(...playersStreams);
      })
    );
  }

Then in the subscription players would be an array of combined values from valueChanges().

getPlayersInRoom(5)
  .subscribe(players => {
    this.players = players;
  })

Please, note that there are more ways to merge values from multiple valueChanges(). Most common are: forkJoin and zip

And there are more methods to map a value on a stream

Hope this helps