在 react-router onEnter 钩子中懒惰地添加新的史诗是一种有效的做法吗? [英] Is it an efficient practice to add new epics lazily inside react-router onEnter hooks?

查看:29
本文介绍了在 react-router onEnter 钩子中懒惰地添加新的史诗是一种有效的做法吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当使用 redux-observable 和 react-router 时,按照文档中的说明异步添加新的史诗是否有意义 here 在路由更改期间react-router的onEnter钩子内?

./epics/index.js

import { BehaviorSubject } from 'rxjs/BehaviorSubject';从'redux-observable'导入{ combineEpics };从'./epic1'导入{epic1}从'./epic2'导入{epic2}出口 const 史诗 $ = 新的 BehaviorSubject(combineEpics(epic1,epic2));export const rootEpic = (action$, store) =>史诗$.mergeMap(史诗=>史诗(行动$,商店));出口 {根史诗};

...routes/SomePath/index.js

import {epic$} from './../../../../epics/index'从'./../../../../epics/epic3'导入{epic3}模块.出口 = {路径:'一些路径',getComponent(位置,cb){require.ensure( [], ( require ) => {cb( null, require( './components/SomePath' ) )})},onEnter() {史诗$.next(epic3)}}

Rxjs 和 redux-observable 非常新.这似乎有效,但想知道:1. 在这种情况下,每次导航到/some-path 时,epic3 是否会再次添加到 rootEpic 中?2. 如果我想在 console.log 中记录哪些史诗被添加到 rootEpic 中,我该怎么做?

<小时>

编辑以回应@JayPhelps

我可以要求澄清几点吗?

  1. registerEpic fn 很好用.我一直在使用 .distinct() 运算符来解决重复的史诗注册问题,如下所示:

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

这是处理懒惰史诗注册的同样有效/好的方法吗?如果不是,您能解释一下原因吗?

  1. 我正在使用具有 require.ensure 和 ES6 导入功能的 create-react-app(它们是不同的导入方式,对吗?),并且基本上,我复制粘贴了 react-router 的巨大应用程序示例,其中包含 require.ensure 代码,但在其他任何地方,我都在文件顶部使用 import 语句.

因此在顶部导入史诗和 registerEpic fn 似乎有效,同时将路径放在 require.ensure 第一个参数中似乎也有效.如果我使用 require.ensure AND import 语句,会不会把事情搞砸?如果我使用 require.ensure 来加载路由需要的所有东西,这是否意味着我删除了该路由组件的嵌套(无路由)组件中的所有 import 语句?

import {epic3 } from './../../epics/epic3'从'./../../epics/epic4'导入{epic4}从 './../../epics/index' 导入 { registerEpic }模块.出口 = {路径:'我的路径',getComponent(位置,完成){require.ensure( [], ( require ) => {registerEpic(addYoutubeEpic)registerEpic(linkYourSiteEpic)完成(空,需要('./components/Edit'))})}}

  1. 关于在 'getComponent' fn 中注册史诗以进行异步加载 - 我认为在这里进行同步加载实际上是可取的,这样路由的组件在注册史诗之前不会呈现.如果用户尝试在路由上执行某些操作,例如获取一些详细信息或提交表单,需要注册史诗,而史诗尚未注册,会发生什么情况?

  2. 在 react-router module.export 声明之外还有其他适合懒惰注册史诗的地方吗?由于我项目中的许多路由不需要登录,因此登录并不总是会触发路由更改.然而,有一堆史诗应该在用户登录后注册.目前,我通过在我的 authReducer 中设置一个令牌变量来以一种非常愚蠢且可能是错误的方式来做,但它所做的只是调用一个函数注册新史诗:

    './../reducers/authReducer.js'从 './../epics/fetchFollowListEpic' 导入 { greatEpic }从'./../epics/fetchMyCollectionsEpic'导入{有用的史诗}//还有一些从 './../epics/index' 导入 { registerEpic }const addEpics = () =>{注册史诗(greatEpic)注册史诗(有用的史诗)...等等}导出默认函数reducer( state = {登录:假,登录:假,错误: '',}, 行动 ) {开关(动作.类型){案例LOGIN_SEQUENCE_DONE":{返回 {...状态,辅助:setAuthToken(action.payload),登录失败:假,addEpics: addEpics(),}}

我认为 loginEpic 是注册史诗的更好地方,而不是 reducer.(我在 github 上找到了 您在 github 上的登录示例,所以它可能看起来很熟悉).这是正确的,还是我完全偏离了基地?我知道 .concat() 是同步的,我知道 redux 是同步的——我不确定 registerEpics() 是否是同步的,但是使用 reducer 来注册史诗 SEEMS 可以正常工作——这是否意味着 registerEpics 是同步的?还是仅仅意味着事情发展得如此之快以至于无关紧要?还是仅仅因为我对 import 语句以及 webpack 如何处理代码拆分的一些基本误解才起作用,我必须进一步研究?

./../epics/loginEpicexport const loginEpic = action$ =>action$.ofType("登录").mergeMap(action =>Observable.fromPromise(axios.post('webapi/login', action.payload)).flatMap(payload =>//连接多个 observables 以便它们按顺序触发Observable.concat(//在 LOGIN_FULILLED 之后Observable.of({ type: "LOGIN_FULFILLED", payload: payload.data.aid }),//****这是我认为我应该在登录后立即注册史诗的地方.下面的ANOTHER_ACTION"取决于这些史诗之一****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({类型:LOGIN_REJECTED",有效载荷:错误,错误:正确})));

解决方案

在这种情况下,每次导航到/some-path 时,epic3 是否会再次添加到 rootEpic 中?

如果您正在使用 react-router 并进行代码拆分,您实际上需要在 getComponent 钩子中添加必要的史诗,不是onEnter 钩子.这是因为 getComponent 钩子允许异步加载该路由所需的资源,不仅是组件,还包括任何史诗、减速器等. 如果您使用 require.ensure(),这告诉 webpack 将这些项目拆分成一个单独的包,这样它们就不会包含在原始条目中——所以不要在其他任何地方导入这些文件,否则你会否定这一点!

粗略地说,这是一个可能看起来像的例子:

routes/SomePath/index.js

import { registerEpic } from 'where/ever/this/is/epics/index.js';导出默认{路径:'一些路径',getComponent(位置,完成){require.ensure(['./components/SomePath', 'path/to/epics/epic3'], require => {//这是您注册史诗、减速器等的地方//属于这个单独包的一部分constepic = require('path/to/epics/epic3').epic3;注册史诗(史诗);done(null, require('./components/SomePath'));});}};

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

import { BehaviorSubject } from 'rxjs/BehaviorSubject';从'redux-observable'导入{ combineEpics };从'./epic1'导入{epic1};从'./epic2'导入{epic2};const epiRegistry = [epic1,epic2];const 史诗 $ = 新的 BehaviorSubject(combineEpics(...epicRegistry));export const registerEpic = (epic) =>{//不要添加已经注册/正在运行的史诗if (epicRegistry.indexOf(epic) === -1) {EpiRegistry.push(史诗);史诗$.next(史诗);}};//你的根史诗需要在史诗 $ 上使用 `mergeMap` 所以任何//新的史诗与其他史诗一起组成export const rootEpic = (action$, store) =>史诗$.mergeMap(史诗=>史诗(行动$,商店));

我创建了一个注册功能,以确保您不会添加已添加的史诗;react-router 将在您每次进入该路由时调用 getComponent ,因此这很重要,因此您不会有两个运行的同一史诗实例.

但是请注意,当涉及到诸如 require('path/to/epics/epic3').epic3 之类的事情时,我的代码做出了一些开箱即用的假设可能是错误的.你的epic3实际上是作为命名属性导出的吗?我认为是这样,但我注意到您正在执行此操作 done(null, require('./components/SomePath')) 我的直觉告诉我,如果您导出该组件,它可能无法正常工作在 ES2015/ES6 模块中使用 export default.如果这是真的,它实际上是在 require('./components/SomePath').default 中找到的——注意 .default!因此,虽然我的代码是总体思路,但您应该特别注意需要的路径、导出的属性等.您说它似乎可以正常工作,所以也许您的设置不同(或者我只是错了,呵呵)

<小时><块引用>

  1. 如果我想在 console.log 中记录哪些史诗被添加到 rootEpic 中,我该怎么做?

最简单的方法就是登录 registerEpic 实用程序:

export const registerEpic = (epic) =>{//不要添加已经注册/正在运行的史诗if (epicRegistry.indexOf(epic) === -1) {EpiRegistry.push(史诗);console.log(`新增史诗:${epic.name}`);史诗$.next(史诗);}};

但您也可以在 epic$ 主题上使用 RxJS .do() 运算符更具体地执行此操作:

export const rootEpic = (action$, store) =>史诗$.do(epic => console.log(`新的史诗添加:${epic.name || '<anonymous>'}`)).mergeMap(史诗=>史诗(行动$,商店));

然而,这将第一次记录 ,因为第一个通过的是 combineEpics(...epicRegistry) 的结果,它结合了将多个史诗合并为一个匿名史诗.

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
                }))
        );

解决方案

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);
  }
};

// your root epic needs to use `mergeMap` on the epic$ so any
// new epics are composed with the others
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.

这篇关于在 react-router onEnter 钩子中懒惰地添加新的史诗是一种有效的做法吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆