Reusing ngrx/effects in Angular (communicating between reducers)

After upgrading my open source project, "Echoes Player", to work with the latest stable angular-cli 1.0 version (wrote an article about it), 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 sharing my take on reusing ngrx/effects in order to communicate between two different reducers.

Preface

The refactor of the code was done in the process of adding a "repeat" feature for the "Echoes Player" controls interface. Until this feature was released, the playlist in the player played the playlist repeatedly. As good as it sounds, the music never stops in "Echoes Player", however, I wanted to add the ability to choose between "party" mode (repeat) and "only once" mode. To make it clear enough: The YouTube player doesn't have a dynamic & customized playlist feature like the one in "Echoes Player".

The Components & Service In This Scenario

I've written before about the various components, services, reducers and side effects in "Echoes Player" and I also published a book on learning reactive programming with Angular & ngrx. However, 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.

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:

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.

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.

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

Lets 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.

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.

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

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.

My new book - "Reactive Programming With Angular & ngrx extensions" - is soon to be release through Apress Media (expected date is June 2017). This book walks through the creation process of a liter version of Echoes while understanding the concepts of reactive programming with RxJs, Angular and ngrx extensions - ngrx/store & ngrx/effects.

If you're looking for Angular Consulting / Front End Consulting or High Quality Javascript Development, please consider to approach via the promotion packages below (no strings attached):

 

  • As you are subscribing to loadNextTrack$ twice, once with @Effect and once with .subscribe, shouldn’t you add a .share() at the end of the effect so you only get one context of execution ?

  • @separ8:disqus you are right.
    thanks for noticing.

  • Yosi Malki

    Oren, Great Article.
    A Few questions if i may:

    1. do you feel that injecting the reducer service into a component is a best practice ?

    2. i’ve read thru the Echoes code – great work!
    i see that sometimes in the component, you dispatch an action, and sometimes you call a function in a service, that dispatch an action.
    for example, in “NowPlayingComponent “, you have the method:
    removeVideo (media) {
    this.nowPlaylistService.removeVideo(media);
    }
    this calls the NowPlayListService, and in the removeVideo function, you dispatch an action
    selectVideo(media) {
    this.store.dispatch({ type: NowPlaylistActions.SELECT, payload: media });

    3. why do you declare your modules, in the index.ts file, and not name the file after the module name ?

    thanks !

  • hi @yosimalki:disqus – thanks for reading through the article and the code.
    I’ll do my best to answer the questions:
    1. if you’re referring to injecting the “effects” service – than – that’s one way of achieving a solution to this kind of challenge – considering the component is a container/smart component. that’s easy to tack and maintain overtime.
    2. using the “nowPlaylistService” for dispatching actions to the store actually follows the “facade” pattern – where the store layer is agnostic to the component and is consumed via an api “service” proxy (or a model). the component is then not depended in the store’s implementation.
    3. i’m following node’s convention for using index.ts as an entry point and a “barrel” – so when importing the module, instead of:
    “import { TheModule } from ‘../src/services/something/something.module'”
    it would be:
    “import { TheModule } from ‘../src/services/something/”
    its just my preference – i.e, easy on the eye when scanning a directory code.
    the module’s name is then reflected in the directory’s name.
    cheers.

  • Yosi Malki

    thanks for the prompt reply !
    as to (2) – you sometimes do dispatch from the component, and sometimes via the ‘facede’ service. why is that ?

  • i was just experimenting with the “nowPlaylistService” and now-playlist reducer. the direct dispatch is of another reducer – that will be probably changed in the future.

  • Yosi Malki

    hi. after digging some more into this.
    what is the flow you would suggest for actions that require some additional BL work including HTTP requests.
    component -> service -> dispatch action -> change state to “loading” for example -> effects , will do the loading from http, and dispatch “loaded” action

    or maybe:
    component -> service -> fetch data ->dispatch action (with the data as the payload) -> change state to “loaded”

    i’ve read articles that tend to go with option 1. wanted to hear your expert thoughts about this

  • I follow the separation of concerns – if the state of “loading” is required to be used anywhere (ui or not), then, I would also use option 1.

  • Yosi Malki

    and if “loading” is not needed. would you load the data in a service or in an effect ?

  • implementing the “service” with http – should be in a service.
    “loading” the data – or calling the service’s method should be used in an effect.

  • Yosi Malki

    just one last thing that bothers me.
    suppose i have a store that includes a data and uistate properties.
    as i understand, a reducer is responsible for one property of the store. this is why we will have 2 in this case.
    i thought that effects are needed when one reducer needs to change the property of another part of the store, that another reducer is responsible for… isnt it ?

    so in a case when i just need to load data from an api, and just mutate the data property of the store, why do i need an effect ?
    is it because reducers are functions and i cannot inject services into them ?
    if this is the case, then every little thing i need to do in my component, i need to: create a method in the service (facade), an action, an effect. this is cumbersome.

  • effects are useful for:
    1. async operations – while actions indicate the various states of the async phases (async_start, async_completed, async_in_progress etc..)
    2. side effects for actions.
    if you find yourself struggling updating a reducer because of another – than perhaps the store for these reducers needs a redesign.
    you don’t have to necessarily create effects for each load. however, following a consistent pattern is useful.