3

I'm trying to do a simple notification system with redux-observable. I'm new to rxjs so I'm having a hard time doing it.

What I'm trying to do is:

  1. Dispatch an intent to display a notification
  2. Detect the intent with an Epic
  3. Dispatch the action that inserts the new notification
  4. Wait 3 seconds
  5. Dispatch another action that deletes the old notification

This is my Epic:

import { NOTIFICATION_DISPLAY_REQUESTED } from '../actions/actionTypes';
import { displayNotification, hideNotification } from '../actions/notifications';

export const requestNotificationEpic = (action$, store) =>
  action$.ofType(NOTIFICATION_DISPLAY_REQUESTED)
    .mapTo(displayNotification(action$.notification))
    .delay(3000)
    .mapTo(hideNotification(action$.notification));

What really happens is that NOTIFICATION_DISPLAY_REQUESTED is dispatched, and 3 seconds later, hideNotification is dispatched. displayNotification never happens.

I could just dispatch displayNotification from the view, delay 3 seconds and then dispatch hideNotification. But later I want to delete the last notification before adding a new one if there are more than 3 active notifications. That's why I dispatch displayNotification manually from inside the epic in this simple case.

So, how do I achieve this? Sorry if this is super simple question, I'm just new to all this and need some help.

Note: I know redux-saga exists, is just that redux-obsevable made more sense to me.

Mathius17
  • 2,412
  • 2
  • 21
  • 33

1 Answers1

4

If you're new to RxJS, this isn't so simple :)

Couple things up front:

Operator chains

An Epic is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out. The functions you chain to transform matching actions are called operators. Chaining operators is a lot like chaining garden hoses or power cords--the values flow from one to the other. It's also very similar to just chaining regular functions like third(second(first())) except that Observables have an additional dimension of time, so the operators are applied on each value that flows through them.

So if you say stream.mapTo(x).mapTo(y) the fact that you firsted mapped to x is made meaningless when you .mapTo(y) since mapTo ignores the source's values and instead just maps it to the one provided.

If instead you used map, it might become more apparant:

stream.map(value => 'a message').map(message => message + '!!!')

Just to be claer, this chaining of operators stuff is RxJS, not specific to redux-observable, which is more a pattern of using idiomatic RxJS with a tiny amount of glue into redux.

action$ is an Observable (technically ActionsObservable)

The argument action$ is an Observable of actions, not an actual action itself. So action$.notification will be undefined. That's one of the reasons people commonly use the dollar sign suffix, to denote it is a stream of those things.

Consider only have 2 actions, not 3

Your example shows you using three actions NOTIFICATION_DISPLAY_REQUESTED and two others to show and hide the notifications. In this case, the original intent action is basically the same as displayNotification() because it would be dispatched synchronously after the other.

Consider only have two actions, one for "show this notification" and another for "hide this notification". While this isn't a rule, it can often simplify your code and increase performance since your reducers don't have to run twice.

This is what it would look like in your case (name things however you'd like, of course):

export const displayNotificationEpic = (action$, store) =>
  action$.ofType(DISPLAY_NOTIFICATION)
    .delay(3000)
    .map(action => hideNotification(action.notification));

// UI code kicks it off some how...
store.dispatch(displayNotification('hello world'));

Your reducers would then receive DISPLAY_NOTIFICATION and then 3 seconds later HIDE_NOTIFICATION (order whatever).

Also, cruicial to remember rom the redux-observable docs:

REMEMBER: Epics run alongside the normal Redux dispatch channel, after the reducers have already received them. When you map an action to another one, you are not preventing the original action from reaching the reducers; that action has already been through them!

Solution

Although I suggest using only two actions in this case (see above), I do want to directly answer your question! Since RxJS is a very flexible library there are many ways of accomplishing what you're asking for.

Here a couple:

One epic, using concat

The concat operator is used subscribe to all the provided Observables one at a time, moving onto the next one only when the current one completes. It "drains" each Observable one at a time.

If we wanted to create a stream that emits one action, waits 3000 ms then emits a different one, you could do this:

Observable.of(displayNotification(action.notification))
  .concat(
    Observable.of(hideNotification(action.notification))
      .delay(3000)
  )

Or this:

Observable.concat(
  Observable.of(displayNotification(action.notification)),
  Observable.of(hideNotification(action.notification))
    .delay(3000)
)

In this case, they have the exact same effect. The key is that we are applying the delay to different Observable than the first--because we only want to delay the second action. We isolate them.

To use inside your epic, you'll need a merging strategy operator like mergeMap, switchMap, etc. These are very important to learn well as they're used very often in RxJS.

export const requestNotificationEpic = (action$, store) =>
  action$.ofType(NOTIFICATION_DISPLAY_REQUESTED)
    .mergeMap(action =>
      Observable.concat(
        Observable.of(displayNotification(action.notification)),
        Observable.of(hideNotification(action.notification))
          .delay(3000)
      )
    );

Two different epics

Another way of doing this would be to create two different epics. One is responsible for maping the first second to the second, the other for waiting 3 seconds before hiding.

export const requestNotificationEpic = (action$, store) =>
  action$.ofType(NOTIFICATION_DISPLAY_REQUESTED)
    .map(action => displayNotification(action.notification));

export const displayNotificationEpic = (action$, store) =>
  action$.ofType(DISPLAY_NOTIFICATION)
    .delay(3000)
    .map(action => hideNotification(action.notification));

This works because epics can match against all actions, even ones that other epics have emitted! This allows clean separation, composition, and testing.

This example (to me) better demonstrates that having two intent actions is unneccesary for this example, but there may be requirements you didn't provide that justify it.


If this was very confusing, I would recommend diving deep into RxJS first. Tutorials, videos, workshops, etc. This is only skimming the surface, it gets much much deeper, but the payout is great for most people who stick with it.

jayphelps
  • 15,276
  • 3
  • 41
  • 54
  • Hi @jayphelps (loved your talk btw!) I actually ended up doing it with the `concat` operator before you answered, so I guess I was on the right track. The reason for the 3 actions instead of just 2 is because I wanted to have a queue of notifications, so a new one wouldn't appear before there was space for it, just [like this one](http://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/38574266#385742660) – Mathius17 May 12 '17 at 20:46