Skip to content

Latest commit

 

History

History
306 lines (243 loc) · 13 KB

Integration_Guide.md

File metadata and controls

306 lines (243 loc) · 13 KB

Mapper Tool Integration Guide

Please note that at this time, the redux-mapper is only designed to integrate closely with React, specifically with react-router. In the future, documentation for integration with other Redux implementations will be included as support for them becomes available. If you would like to volunteer to add such support, please see this documentation for more details on how to contribute changes to this project.

What does the redux-mapper need to know about your project?

The redux-mapper needs to be configured so that it can find:

  • All the script related to each individually swappable reducer module
  • All the individually swappable containers (separated by route)
  • The main container of your application, which cannot be swapped (and usually contains global redux stores)

Additionally, it is necessary to specify:

  • The name of each swappable reducer module
  • If you use redux-sagas, how to distinguish between script for the saga vs. the reducer
  • Where to output the resulting redux map files

And optionally, you can configure:

  • Specific files and folders to ignore (so files not part of your project build, such as unit test files, will not be mapped)
  • Certain debugging options

Specifying the redux-mapper configuration

The preferred way to configure the redux-mapper tool is to create a redux-mapper.json file in the root folder of your project (same location as package.json). Any of the redux-mapper configuration options can be specified as a member of the config JSON object with the argument name as the key.

Here is an example of a minimal configuration:

{
  "config": {
    "basePath": "app/scripts",
    "containerPaths": "containers",
    "mainAppPath": "containers/main.jsx",
    "reduxPaths": "redux",
    "actionFilenames": "actions.js",
    "globalReducersOutputPath": "redux/globalReducers.js",
    "reducerMapOutputPath": "redux/reducerMap.js",
  }
}

How do I decide what to place in my configuration file?

The above example configuration corresponds to a project folder structure like this:

project_root/package.json
project_root/app
project_root/app/scripts
project_root/app/scripts/containers
project_root/app/scripts/containers/main.jsx
project_root/app/scripts/redux
project_root/app/scripts/redux/store1
project_root/app/scripts/redux/store1/actions.js
project_root/app/scripts/redux/store2
project_root/app/scripts/redux/store2/actions.js

In such a structure, the root folder of the project is project_root (where package.json is located). All the buildable frontend scripts for the project are found in subfolder app/scripts as compared to the project root folder. This root script folder is be referred to as the base folder. All containers are then found under there, in a containers subfolder, while all the redux stores are also found under there in a redux subfolder.

The specific structure of your script under the containers folder doesn't matter - you can place your differing container routes in subfolders, or place them all in the containers folder itself. Also, the scripts within containers are free to reference whatever scripts in your project they want (and use redux stores from any of them), since the redux-mapper will follow all imports or requires made within those files to find any usage of redux stores in any subcomponent. This frees you to directly work with redux stores in any of your components, even shared components, without the need to directly keep track of all the routes using them for hot-module-reloading swap purposes.

However, the specific structure of your script under the redux folder DOES matter in the example configuration, in that it is necessary for each redux store under the specified redux subfolder to have its action and reducer files in the same folder, and it is also necessary that the action file for all stores uses the filename actions.js. Such a redux folder configuration is called a native redux-mapper folder structure.

Note that such native folder structures can be supported with the containers and redux script interspersed, like this:

project_root/package.json
project_root/app
project_root/app/scripts
project_root/app/scripts/containers
project_root/app/scripts/containers/main/main.jsx
project_root/app/scripts/containers/container1/store1
project_root/app/scripts/containers/container1/store1/actions.js
project_root/app/scripts/containers/container2/store2
project_root/app/scripts/containers/container2/store2/actions.js
project_root/app/scripts/containers/container3/store3
project_root/app/scripts/containers/container3/store3/actions.js

...simply by changing reduxPaths in the above configuration to be the same as containerPaths. As long as all redux stores are in their own folder, and action files always use the filename action.js in those folders.

If your project has a redux folder structure which is not considered native, which means either that more than redux store is specified in the same folder or varying filenames are used for action or reducer files, then it is necessary to place a clue in each action file allowing the redux-mapper to know that it has found an action file, and which redux store that action file is related to. An example of this is:

export const PRM_ACTION_FILE_FOR_REDUCER = 'widgetStore';
Special note about redux-saga

If you use sagas from redux-saga, then it is necessary to distinguish the saga files from the reducer files at hot module swap time. If you have a native implementation and always use the same filename for your saga file, you can specify it in redux-mapper.json as sagaFilename:

{
  "config": {
    "sagaFilename": "sagas.js"
  }
}

If, however, you have a non-native implementation, or varying filenames for your saga files, you need to provide a clue in each saga file allowing the redux-mapper to know that it has found a saga file, and which redux store that saga file is related to. An example of this is:

export const PRM_SAGA_FILE_FOR_REDUCER = 'widgetStore';
Additional configuration options

An exhaustive list of additional configuration options can be found here.

Specifying PRM_REDUCER_NAME constants in your reducer files

It is necessary for the redux-mapper to know the name of each redux store, which corresponds to the name of the member off the redux state where the store contents live (e.g. state.widgetStore). This name can't be automatically determined by the redux-mapper from inspecting the reducer file, so it is necessary to specify it, in your reducer files, like this:

export const PRM_REDUCER_NAME = 'widgetStore';

The redux-mapper will then be able to include this name in the reducerMap file for each reference to that reducer found in a container, which is necessary to provide upon hot module swap of the reducer.

Integrating the redux-mapper tool into your project build

If you use redux-mapper.json to specify configuration (the preferred method), then you simply need to execute the hmr-redux-mapper script with no arguments at some point early in your build process (before webpack or browserify), since those modules need to bundle the files generated by the redux-mapper).

The command to execute the redux-mapper tool from your project is:

node ./node_modules/hmr-redux-mapper/bin/hmr-redux-mapper

An example configuration to do this in an npm-based build process would be to add these entries to package.json:

"scripts": {
   ...
   "redux-mapper": "node ./node_modules/hmr-redux-mapper/bin/hmr-redux-mapper",
   "prebuild": "npm run build:clean && npm run redux-mapper",
   ...
},

Importing global redux stores

Global redux stores, or redux stores which are referenced from the non-swappable main application file, are output to the path specified using the configuration option globalReducersOutputPath. An example of this file is:

/* AUTOGENERATED FILE - DO NOT MODIFY */
/* generated by HMR_ReduxMapper */
/* https://github.com/paulbrom/hmr-redux-mapper */
import store4 from "app/scripts/redux/store4/reducer.js"
import store7 from "app/scripts/redux/store7/reducer.js"

export default {
  store4,
  store7,
};

This file can simply be imported and its contents sent to Redux's combineReducers along with any global reducers you didn't write (such as react-router's routerReducer).

Importing swappable redux stores

Inside your react-router createRoutes implementation, import both the reduxMapperLoader provided with this package as well as the reducer map file generated to the path specified using the configuration option reducerMapOutputPath.

Initialize the reduxMapperLoader by calling it with a configuration object. That object must be passed the following members:

  • store - the redux store passed to createRoutes
  • createReducer - your redux createReducer() implementation, which returns the combined reducers and takes the async (swappable) reducers as the first parameter
  • reducerMap - the reducer map file generated by the redux-mapper

Optionally, it can also be passed these members:

  • injectReducer - a custom function to inject the async reducer on swap
  • injectSaga - a custom function to inject the async redux saga on swap
  • loadModule - a custom function to load a script module on swap
  • errorLoading - a custom function to receive errors on module load

The default implementation of the above functions that you can override can be found in the source file here.

The reduxMapperLoader will return a getComponentFromReduxMapper function which can be passed to any getComponent handler in your createRoutes implementation. It requires only one argument, the path to the container module that corresponds to the route. All the work to inject the necessary reducers and sagas on swap will be handled by logic within reduxMapperLoader and the contents of the mapper file.

The example integration with createRoutes in react-boilerplate is as follows:

+import reduxMapperLoader from 'hmr-redux-mapper';
+import reducerMap from './reducerMap';

export default function createRoutes(store) {
   // create reusable async injectors using getAsyncInjectors factory
   const { injectReducer, injectSagas } = getAsyncInjectors(store);

+  const getComponentFromReduxMapper = reduxMapperLoader({
+    store,
+    createReducer,
+    reducerMap,
+    injectReducer,
+    injectSagas,
+    loadModule,
+    errorLoading,
+  });
+
   return [
     {
       path: '/',
       name: 'home',
-      getComponent(nextState, cb) {
-        const importModules = Promise.all([
-          import('containers/HomePage/reducer'),
-          import('containers/HomePage/sagas'),
-          import('containers/HomePage'),
-        ]);
-
-        const renderRoute = loadModule(cb);
-
-        importModules.then(([reducer, sagas, component]) => {
-          injectReducer('home', reducer.default);
-          injectSagas(sagas.default);
-
-          renderRoute(component);
-        });
-
-        importModules.catch(errorLoading);
-      },
+      getComponent: getComponentFromReduxMapper('./containers/HomePage/index.js'),
     }, {
       path: '/features',
       name: 'features',
-      getComponent(nextState, cb) {
-        import('containers/FeaturePage')
-          .then(loadModule(cb))
-          .catch(errorLoading);
-      },
+      getComponent: getComponentFromReduxMapper('./containers/FeaturePage'),
     }, {
       path: '*',
       name: 'notfound',
-      getComponent(nextState, cb) {
-        import('containers/NotFoundPage')
-          .then(loadModule(cb))
-          .catch(errorLoading);
-      },
+      getComponent: getComponentFromReduxMapper('./containers/NotFoundPage'),
     },
   ];
 }

Troubleshooting

This should be all you need - once the redux-mapper tool is running in your build, it should issue errors in many misconfiguration cases, but it may fail to find all your containers and redux stores without reporting errors if you don't specify the widest possible search paths for swappable containers and redux stores in the configuration, or if you fail to specify PRM_REDUCER_NAME in all reducers, or if you have a "non-native" folder configuration and did not specify PRM_ACTION_FILE_FOR_REDUCER in all your action files.

In the event of problems, you can turn on verbose logging fro the tool by specifying verboseLogging in redux-mapper.json or by adding -v to the hmr-redux-mapper command directly in your build script.

If you are still stumped, feel free to reach out to me directly at [email protected].

GOOD LUCK!!