Source: store.js

// external dependencies
import Immutable from 'immutable';
import isArray from 'lodash/isArray';
import isFunction from 'lodash/isFunction';
import isPlainObject from 'lodash/isPlainObject';
import {
  LOCATION_CHANGE,
  routerReducer,
} from 'react-router-redux';
import {
  applyMiddleware,
  combineReducers,
  compose,
  createStore as createReduxStore,
} from 'redux';
import {combineReducers as combineImmutableReducers} from 'redux-immutable';
import reduxThunk from 'redux-thunk';

// modules
import {getModules} from './state';

// constants
import {
  ARCO_STATE_KEY,
  ERROR_TYPES,
} from './constants';

// utils
import {testParameter} from './utils';

const IMMUTABLE_ROUTING_REDUCER_INITIAL_STATE = Immutable.fromJS({
  locationBeforeTransitions: null,
});

export const immutableRouterReducer = (state = IMMUTABLE_ROUTING_REDUCER_INITIAL_STATE, {payload, type}) => {
  if (type === LOCATION_CHANGE) {
    return state.set('locationBeforeTransitions', payload);
  }

  return state;
};

/**
 * @module store
 */

/**
 * @private
 *
 * @function addWindowUnloadListener
 *
 * @description
 * add a listener to beforeunload to save the state in sessionStorage
 *
 * @param {Object} store state to store in sessionStorage for retrieval on refresh
 */
export const addWindowUnloadListener = (store) => {
  window.addEventListener('beforeunload', () => {
    const state = store.getState();

    window.sessionStorage.setItem(ARCO_STATE_KEY, JSON.stringify(state));
  });
};

/**
 * @private
 *
 * @function createRestorableStateStore
 *
 * @description
 * create a store that will automatically save and restore the state
 * in session storage
 *
 * @param {function} reducers all reducers to be used in the store creation
 * @param {function} enhancers all enhancers to be used in the store creation
 * @param {Object} initialState state to hydrate the store with on creation
 * @returns {Store}
 */
export const createRestorableStateStore = (reducers, enhancers, initialState) => {
  const stateString = window && window.sessionStorage.getItem(ARCO_STATE_KEY);
  const preloadedState = stateString ? JSON.parse(stateString) : {...initialState};
  const store = createReduxStore(reducers, preloadedState, enhancers);

  if (window) {
    addWindowUnloadListener(store);
  }

  return store;
};

/**
 * @private
 *
 * @function getComposedEnhancers
 *
 * @description
 * get the enhancers used in the store based on the middlewares passed
 * and if thunk is to be included
 *
 * @param {Array<function>} middlewares array of middlewares to be applied to the store
 * @param {boolean} hasThunk whether to use redux-thunk middleware
 * @returns {function|undefined}
 */
export const getComposedEnhancers = (middlewares = [], hasThunk) => {
  let enhancers = [...middlewares];

  if (hasThunk) {
    enhancers.unshift(reduxThunk);
  }

  if (!enhancers.length) {
    return;
  }

  // eslint-disable-next-line rapid7/no-trailing-underscore
  const composeEnhancers = (window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;

  return composeEnhancers(applyMiddleware(...enhancers));
};

/**
 * @private
 *
 * @function getReducerMap
 *
 * @description
 * get the map of reducers based on the modules / reducers
 * passed and whether or not to include the routing reducer
 *
 * @param {Array<Object|function>} modules array of modules or reducers to populate the store with
 * @param {boolean} hasHistory whether a history object exists, and therefore should have a router reducer
 * @param {boolean} isImmutable whether store is immutable or not
 * @returns {Object}
 */
export const getReducerMap = (modules, hasHistory, isImmutable) => {
  const moduleMap = modules.reduce((reducers, passedReducer) => {
    const namespace = passedReducer.namespace;
    const module = isFunction(passedReducer) ? passedReducer : getModules(namespace);

    if (!module) {
      return reducers;
    }

    if (isFunction(module)) {
      return {
        ...reducers,
        [namespace]: module,
      };
    }

    return {
      ...reducers,
      [namespace]: module.reducer,
    };
  }, {});

  if (!hasHistory) {
    return moduleMap;
  }

  const routing = !isImmutable ? routerReducer : immutableRouterReducer;

  return {
    ...moduleMap,
    routing,
  };
};

/**
 * @function createStore
 *
 * @description
 * create a store based on the options passed
 *
 * @example
 * import {
 *  createStore
 * } from 'arco';
 *
 * import appModule from 'modules/app';
 * import fooModule from 'modules/foo';
 * import barModule from 'modules/bar';
 *
 * const store = createStore([appModule, fooModule, barModule], {
 *  shouldRestoreState: true
 * });
 *
 * @param {Array<Object|function>} modules array of modules or reducers to use in the store creation
 * @param {boolean} [autoRestore=false] whether the state should be kept in sessionStorage and automatically restored
 * @param {Object} history history object to use for creation of the store
 * @param {Object} [initialState={}] state to hydrate the store with upon creation
 * @param {boolean} [isImmutable=false] whether to use redux-immutable when combining reducers (if using ImmutableJS)
 * @param {Array<Object|function>} [middlewares=[]] array of middlewares to use in the store creation
 * @param {boolean} [thunk=true] whether to include redux-thunk in the middlewares used in the store creation
 * @returns {Store}
 */
export const createStore = (
  modules,
  {autoRestore = false, history, initialState = {}, isImmutable = false, middlewares = [], thunk = true} = {}
) => {
  testParameter(modules, isArray, 'The first parameter must be an array of modules.', ERROR_TYPES.TYPE);
  testParameter(initialState, isPlainObject, 'initialState must be an object.', ERROR_TYPES.TYPE);
  testParameter(middlewares, isArray, 'middlewares must be an array of functions.', ERROR_TYPES.TYPE);

  const reducerCombiner = isImmutable ? combineImmutableReducers : combineReducers;

  const mapOfReducers = getReducerMap(modules, !!history, isImmutable);
  const allReducers = reducerCombiner(mapOfReducers);
  const enhancers = getComposedEnhancers(middlewares, thunk);

  if (!autoRestore) {
    return createReduxStore(allReducers, initialState, enhancers);
  }

  return createRestorableStateStore(allReducers, enhancers, initialState);
};

export default createStore;