Dynamically load redux reducers with react router

2019-03-15 08:42发布

问题:

I'm splitting my code based on components and I want to inject my reducers only when a component loads, rather than stacking them all up from the start in the store.

In react router 3 it was pretty straight forward but I can't seem to get it to work with react router 4.

Here's the reducers and the store:

reducers.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

And I'm using lazy load for the routes.

How do I implement split reducers?

I would like to inject the async reducers something like so:

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

回答1:

In react-router v4, for async injection of reducers, do the following:

In your reducer.js file add a function called createReducer that takes in the injectedReducers as arg and returns the combined reducer:

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

Then, in your store.js file,

import createReducer from './reducers.js';

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

Now, in order to inject reducer in an async manner when your react container mounts, you need to use the injectReducer.js function in your container and then compose all the reducers along with connect. Example component 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 is an excellent source for understanding this whole setup.You can generate a sample app within seconds. The code for injectReducer.js, configureStore.js( or store.js in your case) and in fact this whole configuration can be taken from react-boilerplate. Specific link can be found here for injectReducer.js, configureStore.js.



回答2:

In order to inject reducers asynchronously, in the first step you need to write create store in the format that you mentioned:

Reducers

In reducers, the only difference is to get asyncReducers as the input of createReducer function and use it in the following way for combine reducers.

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

Configure Store

Your configureStore file should look like below. I made a few changes to your structure. First I applied middlewares in enhancers in order to be able to use chrome redux DevTool Extention if it is installed otherwise use redux compose, (and also use reducer hot-reloader for async reducers).

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;

Component

A simple component will be like this. As you see in this component we first connect component to react-redux and can use mapStateToProps and mapDispatchToProps, and then in order to inject the reducer for this file we need two things:

1) the reducer file, 2)inject reducer function

afterwards, we compose connect and reducerInjected to the component.

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

this file can be implemented in quite a few ways. one of the best practices is implemented by react-boilerplate. This is the file which is used to inject reducers into your components; however, this file has one other dependency (getInjectors.js) that can be put in a utils alongside 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),
  };
}

Now everything is set, You have all the functionality such as reducer injection and even support for hot module reducer load in the development stage. However, I highly suggest two things:

  1. It might be a great idea to look at react-boilerplate as it offers a lot of great features implemented with best practices focused on large-scale applications.

  2. If you are planning to have code splitting it means that you are going to have an application with scalability issue. As a result, I recommend not to use redux-thunk and use redux saga instead. And the best solution is to Inject saga middlewares asynchronously and eject saga files as soon as the component is unMounted. This practice can improve your application in several ways.



回答3:

You could inject not only reducers but also sagas, load pages by chunks and make your components really in competent way with its own css and assets (images, icons) no thing global everything is dynamically attached to app. There is a whole philosophy about it - atomic design, and here is a boilerplate which pursues a similar idea:

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

I realize my answer is not sufficiently complete answer but it may give more idea for the next steps.