Является ли эффективной практикой ленивое добавление новых эпиков внутри хуков react-router onEnter?

При использовании redux-observable с react-router имеет ли смысл асинхронно добавлять новые эпики в соответствии с инструкциями в документации здесь внутри хуков onEnter реагирующего маршрутизатора во время изменения маршрута?

./эпикс/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
};

...маршруты/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)

    }
}

Очень новичок в Rxjs и наблюдаемый с редукцией. Кажется, это работает, но интересно: 1. Будет ли в этом случае epic3 снова добавляться к rootEpic каждый раз, когда мы переходим к /some-path ? 2. Если бы я хотел в console.log какие эпики были добавлены в rootEpic, как бы я это сделал?


Отредактировано в ответ @JayPhelps

Могу ли я попросить разъяснений по нескольким пунктам?

  1. В реестре Epic fn прекрасно работает. Я использовал оператор .distinct() для решения проблемы регистрации повторяющихся эпиков следующим образом:

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

Является ли это в равной степени правильным/хорошим способом обработки ленивой эпической регистрации, и если нет, не могли бы вы объяснить, почему?

  1. Я использую приложение create-react-app, в котором есть require.ensure, а также импорт ES6 (это разные способы импорта, верно?), и в основном я скопировал и вставил пример огромных приложений реакции-маршрутизатора, в котором был код require.ensure, но везде я использую операторы импорта в верхняя часть файла.

Таким образом, кажется, что импорт эпиков и registerEpic fn вверху работает, в то время как размещение путей внутри первого аргумента require.ensure также работает. Не запутается ли это, если я использую операторы require.ensure AND import? Если я использую require.ensure для загрузки ВСЕХ потребностей маршрута, значит ли это, что я удаляю все операторы импорта внутри вложенных (без маршрута) компонентов компонента этого маршрута?

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. Что касается регистрации эпиков в 'getComponent' fn для асинхронной загрузки - я подумал, что на самом деле желательно иметь здесь синхронную загрузку, чтобы компонент маршрута не отображался до тех пор, пока эпик не будет зарегистрирован. Что произойдет, если пользователь попытается сделать что-то на маршруте, например, получить подробную информацию или отправить форму, требующую регистрации эпика, а эпик еще не зарегистрирован?

  2. Есть ли какое-либо другое место за пределами объявлений react-router module.export, где можно было бы лениво регистрировать эпики? Поскольку многие маршруты в моем проекте не требуют входа в систему, вход в систему не всегда приводит к изменению маршрута. Тем не менее, есть куча эпиков, которые должны быть зарегистрированы после входа пользователя в систему. В настоящее время я делаю это очень глупо и, вероятно, неправильно, устанавливая переменную токена в моем authReducer, но все, что он делает, это вызывает функцию что регистрирует новые эпики:

    './../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(),
                }
    
            }
    

Я полагаю, что loginEpic будет лучшим местом для регистрации эпиков, а не редьюсера. (Я нашел ваш пример входа в систему на github, поэтому он может показаться вам знакомым). Это правильно, или я совсем не в теме? Я знаю, что .concat() является синхронным, и я знаю, что избыточный код является синхронным — я не уверен, что registerEpics() является синхронным, но использование редюсера для регистрации эпиков КАЖЕТСЯ работает нормально — означает ли это, что registerEpics является синхронным? Или это просто означает, что все происходит так быстро, что это не имеет значения? Или это работает только из-за некоторого существенного непонимания, которое у меня есть об операторах импорта и о том, как webpack обрабатывает разделение кода, которое мне нужно больше исследовать?

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

person claireablani    schedule 23.10.2016    source источник


Ответы (1)


В этом случае будет ли epic3 снова добавляться в rootEpic каждый раз, когда мы переходим к /some-path?

Если вы используете react-router и выполняете разделение кода, вам действительно нужно добавить необходимые эпики внутри вашего хука getComponent, а не хука onEnter. Это связано с тем, что хук getComponent позволяет асинхронно загружать ресурсы, необходимые для этого маршрута, а не только компонент, но также любые эпики, редукторы и т. д. Если вы используете require.ensure(), это говорит веб-пакету разделить эти элементы в отдельный пакет, чтобы они не были включены в исходной записи - так что не импортируйте эти файлы куда-либо еще, иначе вы сведете на нет это!

Вот примерно такой пример того, как это может выглядеть:

маршруты/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'));
    });
  }
};

где/когда/это/это/эпики/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)
  );

Я создал функцию регистрации, которая гарантирует, что вы не добавляете эпик, который уже был добавлен; react-router будет вызывать getComponent каждый раз, когда вы вводите этот маршрут, поэтому это важно, чтобы у вас не было двух запущенных экземпляров одного и того же эпика.

Обратите внимание, однако, что мой код делает некоторые предположения, которые могут быть неверными, когда дело доходит до таких вещей, как require('path/to/epics/epic3').epic3. Ваш epic3 действительно экспортируется как именованное свойство? Я предполагаю, что да, но я заметил, что вы делаете это done(null, require('./components/SomePath')), которое, как подсказывает мне моя интуиция, может работать неправильно, если вы экспортируете этот компонент, используя export default с модулями ES2015/ES6. Если это правда, то на самом деле он находится в require('./components/SomePath').default — обратите внимание на .default! Итак, хотя мой код является общей идеей, вы должны обратить особое внимание на требуемые пути, экспортированные свойства и т. д. Вы сказали, что это работает, поэтому, возможно, ваша настройка отличается (или я просто ошибаюсь, хе-хе)


  1. Если бы я хотел записать в console.log, какие эпики были добавлены в rootEpic, как бы я это сделал?

Проще всего было бы просто войти в утилиту registerEpic:

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

Но вы также можете сделать это более конкретно, используя оператор RxJS .do() для вашей темы epic$:

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

Однако в первый раз будет записано <anonymous>, потому что первый полученный результат — это результат combineEpics(...epicRegistry), который объединяет несколько эпиков в один анонимный эпик.

person jayphelps    schedule 24.10.2016
comment
Спасибо! Я отредактировал свой ответ выше, чтобы запросить дополнительные разъяснения, но я очень ценю, что вы нашли время, чтобы предоставить этот ответ, и он определенно работает. - person claireablani; 25.10.2016
comment
@jay-phelps, вы рекомендуете делать epic$.next(combineEpics(...epicRegistry)) при регистрации нового эпика? Кажется, что если я этого не сделаю, существующие эпики никогда не попадут в цель. - person Ben; 27.10.2016
comment
@jay-phelps Спасибо, очень полезно. - person Jerry Chen; 09.03.2017