Search code examples
reactjsreact-reduxpollingredux-observablerxjs6

How to loop epic call


I have following epic:

import {
  map,
  catchError,
  switchMap,
  takeUntil,
} from 'rxjs/operators';
import {
  FETCH_JOKES,
  jokesReceived,
  FETCH_JOKES_CANCELLED,
  jokesFetchFailed,
} from '../actions/jokes-action';
import { ofType } from 'redux-observable';
import { of, concat } from 'rxjs';
import { ajax } from 'rxjs/ajax';

const getJokes = () =>
  ajax.getJSON('http://api.icndb.com/jokes/random?escape=javascript');

const fetchJokesEpic = action$ => {
  return action$.pipe(
    ofType(FETCH_JOKES),
    switchMap(() =>
      getJokes().pipe(
        map(response => jokesReceived(response)),
        catchError((err, act) => of(jokesFetchFailed(err))),
        takeUntil(action$.pipe(ofType(FETCH_JOKES_CANCELLED)))
      )
    ),
  );
};

export default fetchJokesEpic;

whenever the FETCH_JOKES is dispatched this epic starts ajax call and on success dispatches jokesReceived action or on failure jokesFetchFailed.

I want to loop it so unless cancelled the endpoint will be called again after previous call will finish (success or failure).

I thought I could use another epic taht will react to JOKES_RECEIVED and FETCH_JOKES_ERROR actions and then dispatch FETCH_JOKES action that should start cricle again.

import { ofType } from 'redux-observable';
import {
  delay,
  map,
} from 'rxjs/operators';
import {
  JOKES_RECEIVED,
  FETCH_JOKES_CANCELLED,
  FETCH_JOKES_ERROR,
  fetchJokes,
} from '../actions/jokes-action';

const pollJokes = (action$, state$) => {
  return action$.pipe(
    ofType(JOKES_RECEIVED, FETCH_JOKES_ERROR, FETCH_JOKES_CANCELLED),
    delay(2000),
    map(action => {
      console.log('pollJokes.map', action);
      if(action.type === FETCH_JOKES_CANCELLED)
      { 
        console.log("Cancelled")
        return { type: 'POLL_STOPPED' };
      }
      else {
        // loop
        return fetchJokes();
      }
    })
  );
};
export default pollJokes;

However this doesnt work when I dispatch FETCH_JOKES_CANCELLED action. The action stack looks like this:

@INIT
FETCH_JOKES
JOKES_RECEIVED
FETCH_JOKES
JOKES_RECEIVED
...
FETCH_JOKES_CANCELLED
FETCH_JOKES
JOKES_RECEIVED
POLL_STOPPED
FETCH_JOKES
JOKES_RECEIVED
...

I've tried to add takeUntil to the pollJokes epic but with the same poor results

const pollJokes = (action$, state$) => {
  return action$.pipe(
    ofType(JOKES_RECEIVED, FETCH_JOKES_ERROR, FETCH_JOKES_CANCELLED),
    delay(2000),
    map(action => {
      console.log('pollJokes.map', action);
      if(action.type === FETCH_JOKES_CANCELLED)
      { 
        console.log("Cancelled")
        return { type: 'POLL_STOPPED' };
      }
      else {
        // loop
        return fetchJokes();
      }
    }),


    takeUntil(action$.pipe(ofType(FETCH_JOKES_CANCELLED)))


  );
};

With this apporach the pollJokes epic stops working as soon as the FETCH_JOKES_CANCELLED is dispatched for the first time. Now I know that the epic completed on takeUntil.

What can I do to have this "looping" work? I could I guess implement a middleware that will actually invoke fetchJokes when JOKES_RECEIVED dispatched, but I was searching for a all-rxjs-solution,


Solution

  • You need to use repeat, in order to restart your epic, once it get cancelled by takeUntil

    const pollJokes = (action$, state$) => {
      return action$.pipe(
        ofType(JOKES_RECEIVED, FETCH_JOKES_ERROR, FETCH_JOKES_CANCELLED),
        delay(2000),
        map(action => {
          console.log('pollJokes.map', action);
          if(action.type === FETCH_JOKES_CANCELLED)
          { 
            console.log("Cancelled")
            return { type: 'POLL_STOPPED' };
          }
          else {
            // loop
            return fetchJokes();
          }
        }),
    
    
        takeUntil(action$.pipe(ofType(FETCH_JOKES_CANCELLED))),
        repeat()
    
      );
    };