claireablani claireablani - 1 month ago 5
React JSX Question

Is it an efficient practice to add new epics lazily inside react-router onEnter hooks?

When using redux-observable with react-router, would it make sense to asynchronously add new epics as per the instructions in the documentation here inside onEnter hooks of react-router during route changes?

./epics/index.js

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { combineEpics } from 'redux-observable';

import { epic1 } from './epic1'
import { epic2 } from './epic2'

export const epic$ = new BehaviorSubject(combineEpics(epic1, epic2));
export const rootEpic = (action$, store) =>
epic$.mergeMap(epic =>
epic(action$, store)
);

export {
rootEpic
};


...routes/SomePath/index.js

import { epic$ } from './../../../../epics/index'
import { epic3 } from './../../../../epics/epic3'

module.exports = {
path: 'some-path',
getComponent( location, cb ) {
require.ensure( [], ( require ) => {
cb( null, require( './components/SomePath' ) )
} )
},
onEnter() {
epic$.next(epic3)

}
}


Very new to Rxjs and redux-observable. This seems to work, but wondering:
1. In this case, would epic3 get added again to the rootEpic every time we navigate to /some-path ?
2. If I wanted to console.log which epics have been added into the rootEpic, how would I do that?




Edited in response to @JayPhelps

May I request clarification on a few points?


  1. The registerEpic fn works great. I had been using the .distinct() operator to address the duplicate epic registering issue like so:

    export const epic$ = new BehaviorSubject(combineEpics(epic1, epic2)).distinct()



Is this an equally valid/good way of handling the lazy epic registering, and if not, could you please explain why?


  1. I am using create-react-app which has require.ensure as well as ES6 import (they are different ways of importing right?), and basically, I copy-pasted react-router's huge-apps example which had the
    require.ensure
    code, but everywhere else, I'm using import statements at the top of the file.



So importing the epics and the registerEpic fn at the top seems to work, while putting the paths inside the require.ensure first argument also seems to work. Does it muddy things up if I use require.ensure AND import statements? If I use require.ensure to load EVERYTHING the route needs, does that mean I remove all import statements inside nested (route-less) components of that route's component?

import { epic3 } from './../../epics/epic3'
import { epic4 } from './../../epics/epic4'
import { registerEpic } from './../../epics/index'

module.exports = {
path: 'my-path',
getComponent( location, done ) {
require.ensure( [], ( require ) => {
registerEpic(addYoutubeEpic)
registerEpic(linkYourSiteEpic)
done( null, require( './components/Edit' ) )
} )
}
}



  1. Regarding registering epics in the 'getComponent' fn for async loading - I thought it was actually desirable to have synchronous loading here, so that the route's component doesn't render until the epic has been registered. What happens if the user tries to do something on the route, like fetch some detailed info or submit a form, that requires the epic to be registered, and the epic isn't registered yet?

  2. Is there any other place outside of react-router module.export declarations that would be appropriate to lazily register epics? Since many routes in my project don't require login, logging in doesn't always trigger a route change. However, there are a bunch of epics that should be registered after a user logs in. Currently, I'm doing it in a really stupid and probably wrong way by setting a token variable in my authReducer, but all it does is call a function that registers the new epics:

    './../reducers/authReducer.js'

    import { greatEpic } from './../epics/fetchFollowListEpic'
    import { usefulEpic } from './../epics/fetchMyCollectionsEpic'
    // and a few more
    import { registerEpic } from './../epics/index'

    const addEpics = () => {
    registerEpic(greatEpic)
    registerEpic(usefulEpic)
    ...etc
    }

    export default function reducer( state = {
    loggingIn: false,
    loggedIn: false,
    error: '',
    }, action ) {
    switch ( action.type ) {
    case "LOGIN_SEQUENCE_DONE": {
    return {
    ...state,
    aid: setAuthToken(action.payload),
    loginFailed: false,
    addEpics: addEpics(),
    }

    }



I would imagine that the loginEpic would be a better place to register the epics, instead of the reducer. (I found your login example on github so it might look familiar). Is that correct, or am I totally off base? I know .concat() is synchronous, and I know that redux is synchronous - I'm not sure if registerEpics() is synchronous, but using the reducer to register the epics SEEMS to work okay - does that mean that registerEpics is synchronous? Or does it just mean that things are firing off so quickly that it doesn't matter? Or does it only work due to some essential misunderstanding I have about import statements and how webpack handles code splitting that I have to research more?

./../epics/loginEpic

export const loginEpic = action$ =>
action$.ofType("LOGIN")
.mergeMap(action =>

Observable.fromPromise(axios.post('webapi/login', action.payload))
.flatMap(payload =>
// Concat multiple observables so they fire sequentially
Observable.concat(
// after LOGIN_FULILLED
Observable.of({ type: "LOGIN_FULFILLED", payload: payload.data.aid }),
// ****This is where I think I should be registering the epics right after logging in. "ANOTHER_ACTION" below depends upon one of these epics****
Observable.of({type: "ANOTHER_ACTION", payload: 'webapi/following'}),
Observable.of({type: "LOGIN_SEQUENCE_DONE"}),
)
)
.startWith({ type: "LOGIN_PENDING" })
.takeUntil(action$.ofType("LOGIN_CANCELLED"))
.catch(error => Observable.of({
type: "LOGIN_REJECTED",
payload: error,
error: true
}))
);

Answer

In this case, would epic3 get added again to the rootEpic every time we navigate to /some-path?

If you're using react-router and doing code splitting, you'll actually want to add the necessary epics inside your getComponent hook, not the onEnter hook. This is because the getComponent hook allows async loading of resources necessary for that route, not just the component but also any epics, reducers, etc. If you use require.ensure(), this tells webpack to split these items into a separate bundle so they are not included in the original entry one--so don't import those files anywhere else, otherwise you'll negate this!

So roughly this is an example of what that might look like:

routes/SomePath/index.js

import { registerEpic } from 'where/ever/this/is/epics/index.js';

export default {
  path: 'some-path',

  getComponent(location, done) {
    require.ensure(['./components/SomePath', 'path/to/epics/epic3'], require => {
      // this is where you register epics, reducers, etc
      // that are part of this separate bundle
      const epic = require('path/to/epics/epic3').epic3;
      registerEpic(epic);

      done(null, require('./components/SomePath'));
    });
  }
};

where/ever/this/is/epics/index.js

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { combineEpics } from 'redux-observable';

import { epic1 } from './epic1';
import { epic2 } from './epic2';

const epicRegistry = [epic1, epic2];
const epic$ = new BehaviorSubject(combineEpics(...epicRegistry));

export const registerEpic = (epic) => {
  // don't add an epic that is already registered/running
  if (epicRegistry.indexOf(epic) === -1) {
    epicRegistry.push(epic);
    epic$.next(epic);
  }
};

export const rootEpic = (action$, store) =>
  epic$.mergeMap(epic =>
    epic(action$, store)
  );

I created a registration function that makes sure you're not adding an epic that already has been added; react-router will call getComponent every time you enter that route, so this is important so you don't have two running instances of the same epic.

Take note however, my code makes some assumptions that might be wrong out of box, when it comes to things like require('path/to/epics/epic3').epic3. Is your epic3 actually exported as a named property? I presume so, but I noticed you are doing this done(null, require('./components/SomePath')) which my gut tells me might not be working correctly if you're exporting that component using export default with ES2015/ES6 modules. If that's true, it's actually found at require('./components/SomePath').default--note the .default! So while my code is the general idea, you should take special note of the require paths, exported properties, etc. You said it seems to be working, so perhaps your setup is different (or I'm just wrong hehe)


  1. If I wanted to console.log which epics have been added into the rootEpic, how would I do that?

The easiest would just be to log inside the registerEpic utility:

export const registerEpic = (epic) => {
  // don't add an epic that is already registered/running
  if (epicRegistry.indexOf(epic) === -1) {
    epicRegistry.push(epic);
    console.log(`new epic added: ${epic.name}`);
    epic$.next(epic);
  }
};

But you can also do this more concretely using the RxJS .do() operator on your epic$ subject:

export const rootEpic = (action$, store) =>
  epic$
    .do(epic => console.log(`new epic added: ${epic.name || '<anonymous>'}`))
    .mergeMap(epic =>
      epic(action$, store)
    );

However, this will log <anonymous> the first time, because the first one that comes through is the result of combineEpics(...epicRegistry) which combines multiple epics into a single anonymous epic.

Comments