// 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;