Редукторы с динамической загрузкой с помощью React Router 4

Я разделяю свой код на компоненты, и я хочу вводить свои редукторы только при загрузке компонента, а не складывать их все с самого начала в магазине.

В реактивном маршрутизаторе 3 это было довольно просто, но я не могу заставить его работать с реагирующим маршрутизатором 4.

Вот редукторы и магазин:

redurs.js

import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'

import modalReducer from '../modules/modal'

export default combineReducers({
  routing : routerReducer,
  modal   : modalReducer
})

store.js

import { createStore, applyMiddleware, compose } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import thunk from 'redux-thunk'
import createHistory from 'history/createBrowserHistory'
import rootReducer from './reducers'

export const history = createHistory()

const initialState = {}
const enhancers = []
const middleware = [
  thunk,
  routerMiddleware(history)
]

if (process.env.NODE_ENV === 'development') {
  const devToolsExtension = window.devToolsExtension

  if (typeof devToolsExtension === 'function') {
    enhancers.push(devToolsExtension())
  }
}

const composedEnhancers = compose(
  applyMiddleware(...middleware),
  ...enhancers
)

const store = createStore(
  rootReducer(),
  initialState,
  composedEnhancers
)
export default store

И я использую ленивую загрузку для маршрутов.

Как реализовать раздельные редукторы?

Я хотел бы ввести асинхронные редукторы примерно так:

function createReducer(asyncReducers) {
  return combineReducers({
    ...asyncReducers,
    system,
    router,
  })
}

function injectReducer(store, { key, reducer }) {
  if (Reflect.has(store.asyncReducers, key)) return

  store.asyncReducers[key] = reducer
  store.replaceReducer(createReducer(store.asyncReducers))
}

В чем причина такой сложной настройки? Вашему приложению действительно нужны полиморфные редукторы? Как насчет того, чтобы все они были доступны в одном магазине и использовать правильный для каждого компонента?

timotgl 27.04.2018 12:55

Это может быстро выйти из-под контроля в крупных проектах

S. Schenk 01.05.2018 12:42

@ S.Schenk Удалось ли вам разделить редукторы? У вас есть еще проблемы? Если нет, примите один из ответов, чтобы помочь остальным увидеть вопрос в будущем :)

Anu 02.05.2018 11:34
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
13
3
3 901
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

В реагировать-маршрутизатор v4 для асинхронной инъекции редукторов выполните следующие действия:

В вашем файле reducer.js добавьте функцию с именем createReducer, которая принимает injectedReducers как arg и возвращает объединенный редуктор:

/**
 * Creates the main reducer with the dynamically injected ones
 */
export default function createReducer(injectedReducers) {
  return combineReducers({
    route: routeReducer,
    modal: modalReducer,
    ...injectedReducers,
  });
} 

Затем в вашем файле store.js

import createReducer from './reducers.js';

const store = createStore(
  createReducer(),
  initialState,
  composedEnhancers
);
store.injectedReducers = {}; // Reducer registry

Теперь, чтобы внедрить редуктор асинхронно при монтировании вашего реакционного контейнера, вам нужно использовать функцию injectReducer.js в вашем контейнере, а затем скомпоновать все редукторы вместе с connect. Пример компонента Todo.js:

// example component 
import { connect } from 'react-redux';
import { compose } from 'redux';
import injectReducer from 'filepath/injectReducer';
import { addToDo, starToDo } from 'containers/Todo/reducer';

class Todo extends React.Component {
// your component code here
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);

const addToDoReducer = injectReducer({
  key: 'todoList',
  reducer: addToDo,
});

const starToDoReducer = injectReducer({
  key: 'starredToDoList',
  reducer: starToDo,
});

export default compose(
  addToDoReducer,
  starToDoReducer,
  withConnect,
)(Todo);

React-Boilerplate - отличный источник для понимания всей этой настройки. Вы можете сгенерировать образец приложения за секунды. Код для injectReducer.js, configureStore.js (или store.js в вашем случае) и, по сути, вся эта конфигурация может быть взята из response-шаблона. Здесь можно найти конкретную ссылку для injectReducer.js, configureStore.js.

Чтобы внедрить редукторы асинхронно, на первом этапе вам нужно написать create store в указанном вами формате:

Редукторы

В редукторах единственное отличие состоит в том, чтобы получить asyncReducers в качестве входных данных функции createReducer и использовать его следующим образом для объединяющих редукторов.

function createReducer(asyncReducers) {
  return combineReducers({
    ...asyncReducers,
    system,
    router,
  })
}

Настроить магазин

Ваш файл configureStore должен выглядеть, как показано ниже. Я внес несколько изменений в вашу структуру. Сначала я применил промежуточное ПО в усилителях, чтобы иметь возможность использовать chrome redux DevTool Extension, если оно установлено, в противном случае использовать redux compose (а также использовать reducer hot-reloader для асинхронных редукторов).

import { createStore, applyMiddleware, compose } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import thunk from 'redux-thunk'
import createHistory from 'history/createBrowserHistory'
import rootReducer from './reducers'

export const history = createHistory()

const initialState = {}

const middleware = [
  thunk,
  routerMiddleware(history)
]

const enhancers = [
  applyMiddleware(...middlewares),
];


/* eslint-disable no-underscore-dangle */
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
    // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading
    // Prevent recomputing reducers for `replaceReducer`
    shouldHotReload: false,
  })
  : compose;
/* eslint-enable */


const store = createStore(
   rootReducer(),
   initialState,
   composeEnhancers(...enhancers)
);

// Extensions
store.injectedReducers = {}; // Reducer registry

/ Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
  module.hot.accept('./reducers', () => {
    store.replaceReducer(createReducer(store.injectedReducers));
  });
}

export default store;

Составная часть

Простой компонент будет таким. Как вы видите в этом компоненте, мы сначала компонент connect для реакции-редукции и можем использовать mapStateToProps и mapDispatchToProps, а затем, чтобы внедрить редуктор для этого файла, нам нужны две вещи:

1) файл-редуктор, 2) вводить функцию редуктора

после этого мы составляем connect и reducerInjection в компонент.

import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import reducerForThisComponent from './reducer';
import injectReducer from 'path_to_recuer_injector';

const Component = (props)=><div>Component</div>

function mapStateToProps (state){
   return {}
}
const withConnect = connect(mapStateToProps);
const withReducer = injectReducer({ key: 'login', reducerForThisComponent });

export default compose(
  withReducer,
  withConnect,
)(Component);

injectReducer.js

этот файл можно реализовать несколькими способами. одна из лучших практик реализована с помощью response-шаблона. Это файл, который используется для встраивания редукторов в ваши компоненты; однако у этого файла есть еще одна зависимость (getInjectors.js), которую можно поместить в утилиты вместе с injectReducer.js.

import React from 'react';
import PropTypes from 'prop-types';
import hoistNonReactStatics from 'hoist-non-react-statics';

import getInjectors from './getInjectors';

/**
 * Dynamically injects a reducer
 *
 * @param {string} key A key of the reducer
 * @param {function} reducer A reducer that will be injected
 *
 */
export default ({ key, reducer }) => (WrappedComponent) => {
  class ReducerInjector extends React.Component {
    static WrappedComponent = WrappedComponent;
    static contextTypes = {
      store: PropTypes.object.isRequired,
    };
    static displayName = `withReducer(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;

    componentWillMount() {
      const { injectReducer } = this.injectors;

      injectReducer(key, reducer);
    }

    injectors = getInjectors(this.context.store);

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return hoistNonReactStatics(ReducerInjector, WrappedComponent);
};

getInjectors.js

import invariant from 'invariant';
import isEmpty from 'lodash/isEmpty';
import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';

import isString from 'lodash/isString';

import createReducer from '../reducers'; //The createStoreFile


/**
 * Validate the shape of redux store
 */
function checkStore(store) {
  const shape = {
    dispatch: isFunction,
    subscribe: isFunction,
    getState: isFunction,
    replaceReducer: isFunction,
    runSaga: isFunction,
    injectedReducers: isObject,
    injectedSagas: isObject,
  };
  invariant(
    conformsTo(store, shape),
    '(app/utils...) injectors: Expected a valid redux store'
  );
}


export function injectReducerFactory(store, isValid) {
  return function injectReducer(key, reducer) {
    if (!isValid) checkStore(store);

    invariant(
      isString(key) && !isEmpty(key) && isFunction(reducer),
      '(app/utils...) injectReducer: Expected `reducer` to be a reducer function'
    );

    // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
    if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return;

    store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
    store.replaceReducer(createReducer(store.injectedReducers));
  };
}

export default function getInjectors(store) {
  checkStore(store);

  return {
    injectReducer: injectReducerFactory(store, true),
  };
}

Теперь все настроено, у вас есть все функции, такие как внедрение редуктора и даже поддержка загрузки горячего модуля-редуктора на этапе разработки. Однако я настоятельно рекомендую две вещи:

  1. Было бы неплохо взглянуть на react-boilerplate, поскольку он предлагает множество замечательных функций, реализованных с использованием лучших практик, ориентированных на крупномасштабные приложения.

  2. Если вы планируете разделение кода, это означает, что у вас будет приложение с проблемой масштабируемости. В результате я рекомендую не использовать redux-thunk и вместо этого использовать redux saga. И лучшее решение - это установить Inject saga middlewares asynchronously и извлечь файлы саги, как только компонент будет размонтирован. Эта практика может улучшить ваше приложение несколькими способами.

Вы можете вводить не только редукторы, но и саги, загружать страницы по частям и делать свои компоненты действительно компетентным способом с помощью собственных CSS и ресурсов (изображений, значков), ничего глобального, все динамически прикрепляется к приложению. В этом есть целая философия - атомарный дизайн, и вот шаблон, который преследует аналогичную идею:

https://github.com/react-boilerplate/react-boilerplate

Я понимаю, что мой ответ не является достаточно полным, но он может дать больше идей для следующих шагов.

Другие вопросы по теме