www.apress.com

11/05/2017

Reusing ngrx/effects in Angular (communicating between reducers)

By Oren Farhi

After upgrading my open source project "Echoes Player" to work with the latest stable angular-cli 1.0 version, I set down to refactor the application's code. I always like to look at implementations few times and experiment with several approaches. This time, I wanted to take advantage of ngrx/effects observables and understand how those can be reused. In this post I'm going to share my take on reusing ngrx/effects in order to communicate between two different reducers.

I began the refactor of the code by adding a repeat feature for the Echoes Player controls interface. Until this feature was released, the playlist in the player played the playlist repeatedly. I wanted to add the ability to choose between "party" mode (repeat) and "only once" mode. These are the main components and services that take part in the sequence for loading the next track:

  1. Now-Playing Component
  2. App-Player Component (Formerly - "Youtube-Player")
  3. Reducers:
    1. now-playlist reducer
    2. app-player reducer (formerly - "youtube-player" reducer)
  4. Effects:
    1. now-playlist effects
    2. app-player effects (formerly - "youtube-player" effects)
  5. Services:
    1. Youtube Player Service

Sequence For Loading The Next Track

The scenario for loading the next track is handled by a side effect to an action. These are the steps in the sequence:

  1. YouTube player (3rd party) API fires an event when the player stops playing the media.
  2. The "YoutubePlayerService" in the app, emits a state change event with the status of "YT.PlayerState.ENDED".
  3. The "AppPlayer" component, emits a "MEDIA_ENDED" action to the now-playlist reducer.
  4. The next track is selected with regards to the "repeat" settings using a side effect with the action of "NowPlaylistActions.SELECT" action.
  5. The last action to be fired is for playing the selected media (if selected) with the "AppPlayerActions.PLAY" action.

Steps 4 and 5 are resolved in two different reducers. We could inject the "AppPlayerActions" into the now-playlist effects class, however, that breaks the "Single Responsibility Rule (SRP)" for this effects class - as it supposed to serve the now-playlist actions scenarios. The "AppPlayer" Component is responsible for emitting the "MEDIA_ENDED" event. In the code snippet below, I'm using the youtube-player component (another open source component available in npm that I released as a side effect of this player) custom "change()" event to update the application with the player's state:

  1. The "onPlayerChange()" action updates the player reducer with the current state of the player.
  2. The "trackEnded()" action starts the process for checking if a next track should be played in the playlist.

@Component({

  selector: 'app-player',

  template: `

  <section

    [class.show-youtube-player]="(player$ | async).showPlayer"

    [class.fullscreen]="(player$ | async).isFullscreen">

    <div class="yt-player ux-maker">

    ...

      <youtube-player class="nicer-ux"

        (ready)="setupPlayer($event)"

        (change)="updatePlayerState($event)"

      ></youtube-player>

    </div>

    ...

  </section>

  `

})

export class AppPlayerComponent implements OnInit {

 

  constructor(

    private playerService: YoutubePlayerService,

    public nowPlaylistService: NowPlaylistService,

    private playerActions: AppPlayerActions,

    private store: Store<EchoesState>

  ) {

  }

 

  updatePlayerState (event) {

    this.playerService.onPlayerStateChange(event);

    if (event.data === YT.PlayerState.ENDED) {

      this.nowPlaylistService.trackEnded();

    }

  }

}

When the "repeat" feature wasn't available, playing the next track was quite easy: the player should emit a play action and take the currently selected media to be played using the Youtube Player Service. Actually, this was the previous "updatePlayerState()" code:

updatePlayerState (event) {

    this.playerService.onPlayerStateChange(event);

    if (event.data === YT.PlayerState.ENDED) {

      this.nowPlaylistService.trackEnded();

      this.store.dispatch(this.playerActions.playVideo(this.nowPlaylistService.getCurrent()));

    }

}

This worked for the purpose of just keep playing the next available track. However, to support the "repeat" feature, I had to think of another way and I wanted to think in a reactive programming style while reusing code that is already written.

Understanding the "MEDIA_ENDED" Side Effect

The "trackEnded()" method invoked the "MEDIA_ENDED" action. This action is handled in the "now-playlist" reducer which runs the "selectNextOrPreviousTrack()". This function updates the "selectedId" property in the reducer. If "repeat" is "on" and it's the end of the playlist, the playlist should not select the first track as the new selected track to play - an empty string indicates this state.

function selectNextOrPreviousTrack(state: NowPlaylistInterface, filter: string): NowPlaylistInterface {

  const videosPlaylist = state.videos;

  const currentId = state.selectedId;

  const indexOfCurrentVideo = videosPlaylist.findIndex(video => currentId === video.id);

  const isCurrentLast = indexOfCurrentVideo + 1 === videosPlaylist.length;

  const nextId = isCurrentLast

    ? getNextIdForPlaylist(videosPlaylist, state.repeat, currentId)

    : selectNextIndex(videosPlaylist, currentId, filter, state.repeat);

  return Object.assign({}, state, { selectedId: nextId });

}

 

function getNextIdForPlaylist(videos: GoogleApiYouTubeVideoResource[], repeat: boolean, currentId: string = '') {

  let id = '';

  if (videos.length && repeat) {

    id = videos[0].id;

  }

  return id;

}

The side effect that runs after the selected media has been updated, is supposed to emit a "selectVideo" action when the right conditions exists. To achieve that, first, it takes the latest state for the selected media object. Next, the "filter" operator checks whether the selected media is valid for playing the video - when the playlist is over, the "selectedId" property is set to an empty string, so in this case, there would not be any valid media object to play. This means that the side effect will not emit the "selectVideo" action if the filter function result is an invalid condition for selecting the next video.

@Effect()

loadNextTrack$ = this.actions$

    .ofType(NowPlaylistActions.MEDIA_ENDED)

    .map(toPayload)

    .withLatestFrom(this.store.let(getSelectedMedia$))

    .filter((states: [any, GoogleApiYouTubeVideoResource]) => states[1] && states[1].hasOwnProperty('id'))

    .map((states: [any, GoogleApiYouTubeVideoResource]) => {

      return this.nowPlaylistActions.selectVideo(states[1]);

    }).share();

This sequence just selects the next video in the playlist - it doesn't play it. Now it's time to integrate the actual action which triggers the "PLAY" action.

Reusing Effect & Communication Between Reducers

Let’s understand the result of creating a side effect - aka - invoking the "@Effect()" decorator. Eventually, the "this.actions$" sequence returns an observable object to the "loadNextTrack$" property. This means that the "loadNextTrack$" can be subscribed with an observer. Notice that I added the "share()" operator at the end of the Effect's sequence. As reader Brett Coffin suggested, since both the store and the following subscribe to this effect, using "share()" will create only one execution for both subscription rather than running the effect chain twice.

Each "Effects" class is an injectable service. This gives us the opportunity to inject the "NowPlaylistEffects" class to the constructor of the "AppPlayer" component.

constructor(

    private playerService: YoutubePlayerService,

    public nowPlaylistService: NowPlaylistService,

    private playerActions: AppPlayerActions,

    private store: Store<EchoesState>,

    private nowPlaylistEffects: NowPlaylistEffects

  ) {

  }

Now, in the "ngOnInit" life cycle, the component subscribes to the "loadNextTrack$" side effect and triggers a "PLAY" action with the payload of this action - the next selected media to play. Since the side effect filters scenarios where the track is last and repeat is not on, this subscription won't be triggered. It's important to note that defining this behavior - playing video after this side effect has triggered - will always happen - so, it's important to design and define the requirement.

// AppPlayerComponent

ngOnInit() {

    this.nowPlaylistEffects.loadNextTrack$

      .subscribe((action) => this.playVideo(action.payload));

}

...

playVideo (media: GoogleApiYouTubeVideoResource) {

    this.store.dispatch(this.playerActions.playVideo(media));

}

Since the "AppPlayerComponent" is a container component (SMART), there's no need to unsubscribe from this subscription. Otherwise, this subscription should be disposed as:

// AppPlayerComponent

private nowPlaylistSub: Subscription;

 

ngOnInit() {

    this.store.dispatch(this.playerActions.reset());

    this.nowPlaylistSub = this.nowPlaylistEffects.loadNextTrack$

      .subscribe((action) => this.playVideo(action.payload));

}

 

ngOnDestroy() {

    this.nowPlaylistSub.unsubscribe();

}

That sums up the reuse of the "NowPlaylist" effects and the concept of communicating between two reducers or more. "Echoes Player" is an open source project - feel free to suggest better alternatives, feature requests and other suggestions as well.

About the Author

Oren Farhi is a Senior Front End Engineer & Javascript Consultant at Orizens (http://orizens.com). He consults on how to approach front end development and create maintainable code as well as front end project development by demand.  He studied Computer Science and Management at The Open University. 

Oren believes in producing easy maintainable code for applications. He follows the principles of reactive programming, best practices of software architecture, and by creates modular and testable code. He likes to keep code and app structure organized to let other developers easily understand and further extend it.  

Oren is proficient with front end development and is working with various solutions such as Angular, Typescript, ngrx, react, redux, sass, webpack, jasmine, nodejs and JavaScript based Build Tools that solves challenges well. Aside from exploring Web development and blogging, Oren enjoys spending time with his family, playing guitar, meditating, traveling and watching TV series and movies.

This blog post is taken from Oren Farhi’s blog. Oren’s new book, Reactive Programming with Angular and ngrx, is out now.