lucid-ui
Version:
A UI component library from AppNexus.
211 lines • 8.91 kB
JavaScript
import _ from 'lodash';
import { createSelector } from 'reselect';
import { reduceSelectors, safeMerge } from './state-management';
import { logger, isDevMode } from './logger';
/**
* Marks a function on the reducer tree as a thunk action creator so it doesn't
* get incorporated into the redux reducer
*
* @return {function} with `isThunk` set to `true`
*/
export function thunk(fn) {
fn.isThunk = true;
return fn;
}
export function getReduxPrimitives({ initialState, reducers, rootPath = [], rootSelector = _.identity, selectors, }) {
/* istanbul ignore if */
if (isDevMode && _.isEmpty(rootPath)) {
logger.warn(`\`getReduxPrimitives\` warning:
\`rootPath\` is empty`);
}
/* istanbul ignore if */
if (isDevMode && !initialState) {
logger.warn(`\`getReduxPrimitives\` warning:
Missing \`initialState\` for component at \`rootPath\` ${_.isArray(rootPath) ? rootPath.join(',') : rootPath}
Components should have an \`initialState\` property or a \`getDefaultProps\` defined.
`);
}
// we need this in scope so actionCreators can refer to it
let dispatchTree;
const reducer = createReduxReducer(reducers, initialState, rootPath);
const selector = selectors ? reduceSelectors(selectors) : _.identity;
const rootPathSelector = (state) => _.isEmpty(rootPath) ? state : _.get(state, rootPath);
const mapStateToProps = createSelector([rootPathSelector], (rootState) => rootSelector(selector(rootState)));
// dispatch could be store.dispatch's return value or an async lib's return value?
const mapDispatchToProps = (dispatch) => getDispatchTree(reducers, rootPath, dispatch);
const devModeMapStateToProps = (rootState) => {
/* istanbul ignore if */
if (isDevMode && !_.has(rootState, rootPath)) {
logger.warn(`\`getReduxPrimitives\` warning:
\`rootPath\` ${rootPath} does not exist in the redux store.
Make sure your \`rootPath\` is correct.
`);
}
return mapStateToProps(rootState);
};
return {
reducer,
connectors: [
isDevMode ? devModeMapStateToProps : mapStateToProps,
mapDispatchToProps,
mergeProps,
],
};
/**
* @param {function} node - a node in the the reducer tree, either a reducer or a thunk
* @param {string[]} path - the path to the reducer in the reducer tree
* @param {string[]} rootPath - array of strings representing the path to local state in global state
* @return {function} action creator that returns either an action or a thunk
*/
function createActionCreator(node, rootPath, path) {
if (node.isThunk) {
return function thunk(...args) {
return function thunkInner(dispatch, getState, ...rest) {
const pathToLocalDispatchTree = _.slice(path, rootPath.length, -1);
const pathToLocalState = _.dropRight(path);
const localDispatchTree = _.isEmpty(pathToLocalDispatchTree)
? dispatchTree
: _.get(dispatchTree, pathToLocalDispatchTree);
const getLocalState = _.isEmpty(pathToLocalState)
? getState
: () => _.get(getState(), pathToLocalState);
return node(...args)(localDispatchTree, getLocalState, dispatch, getState, ...rest);
};
};
}
return function actionCreator(...args) {
const [payload, ...meta] = isDevMode ? cleanArgs(args) : args;
return {
type: path.join('.'),
payload,
meta,
};
};
}
/**
* Walks the reducer tree and generates a tree of action creators that correspond to each reducer
* @param {Object} reducers - a tree of lucid reducers
* @param {string[]} rootPath - array of strings representing the path to local state in global state
* @returns {Object} action creator tree
*/
function createActionCreatorTree(reducers, rootPath, path = rootPath) {
return _.reduce(reducers, (memo, node, key) => {
const currentPath = path.concat(key);
return {
...memo,
[key]: _.isFunction(node)
? createActionCreator(node, rootPath, currentPath)
: createActionCreatorTree(node, rootPath, currentPath),
};
}, {});
}
/**
* Walks the reducer tree and generates an action creator tree, then binds dispatch to each node
* @param {Object} reducers - a tree of lucid reducers
* @param {string[]} rootPath - array of strings representing the path to local state in global state
* @param {function} dispatch - the redux store's `dispatch` function
*/
function getDispatchTree(reducers, rootPath, dispatch) {
const actionCreatorTree = createActionCreatorTree(reducers, rootPath);
dispatchTree = bindActionCreatorTree(actionCreatorTree, dispatch);
/* istanbul ignore if */
if (isDevMode) {
//@ts-ignore
window.lucidReduxUtil = window.lucidReduxUtil || {};
//@ts-ignore
window.lucidReduxUtil[rootPath] = {
actionCreatorTree,
dispatchTree,
};
}
return dispatchTree;
}
}
function createReduxReducerTree(reducers, path = []) {
return _.reduce(reducers, (memo, node, key) => {
// filter out thunks from the reducer tree
if (node.isThunk) {
return memo;
}
const currentPath = path.concat(key);
return {
...memo,
[key]: _.isFunction(node)
? function reduxReducer(state, action) {
const { type, payload, meta = [] } = action;
if (_.isUndefined(state) || type !== currentPath.join('.')) {
return state;
}
return node(state, payload, ...meta);
}
: createReduxReducerTree(node, currentPath),
};
}, {});
}
/**
* Returns a function that calls every reducer in the reducer tree with the reducer's local state and action
* @param {Object} reduxReducerTree - tree of redux reducers with signature `(state, action) => state`
* @param {Object} initialState - the initial state object that the reducer will return
* @return {function} the redux reducer
*/
function createReducerFromReducerTree(reduxReducerTree, initialState) {
return function reduxReducer(state, action) {
if (_.isUndefined(state)) {
return initialState;
}
return _.reduce(reduxReducerTree, (state, node, key) => {
return {
...state,
...(_.isFunction(node)
? node(state, action)
: {
[key]: createReducerFromReducerTree(node, {})(state[key], action),
}),
};
}, state);
};
}
/**
* Generates a redux reducer from a tree of lucid reducers
* @param {Object} reducers - a tree of lucid reducers
* @param {Object} initialState - the initial state object that the reducer will return
* @param {string[]} rootPath - array of strings representing the path to part of global state this reducer applies to
* @return {function} the redux reducer
*/
function createReduxReducer(reducers, initialState, rootPath) {
const reducerTree = createReduxReducerTree(reducers, rootPath);
return createReducerFromReducerTree(reducerTree, initialState);
}
/**
* Binds redux store.dispatch to actionCreators in a tree
* @param {Object} actionCreatorTree - a tree of redux action creator functions
* @param {function} dispatch - the redux store's `dispatch` function
* @param {string[]} path - array of strings representing the path to the action creator
*/
function bindActionCreatorTree(actionCreatorTree, dispatch, path = []) {
return _.reduce(actionCreatorTree, (memo, node, key) => ({
...memo,
[key]: _.isFunction(node)
? function boundActionCreator(...args) {
const action = actionCreatorTree[key](...args);
return dispatch(action);
}
: bindActionCreatorTree(node, dispatch, path.concat(key)),
}),
// @ts-ignore
{});
}
/**
* Merges state, dispatchTree, and ownProps into a single props object
* @param {Object} state
* @param {Object} dispatchTree
* @param {Object} ownProps
* @return {Object}
*/
const mergeProps = _.memoize((state, dispatchTree, ownProps) => {
return _.mergeWith({}, state, dispatchTree, ownProps, safeMerge);
});
export function cleanArgs(args) {
return _.has(_.last(args), 'event') ? _.dropRight(args) : args;
}
//# sourceMappingURL=redux.js.map