UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

341 lines (318 loc) 9.96 kB
import _ from 'lodash'; import { createSelector } from 'reselect'; import { reduceSelectors, safeMerge } from './state-management'; import { logger, isDevMode } from './logger'; type Funk = (...args: any) => any; interface IThunk { isThunk?: boolean; } type FunkThunk = Funk & IThunk; /** * 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: FunkThunk) { fn.isThunk = true; return fn; } /** * Creates a redux reducer and connectors (inputs to redux-react's `connect`) * * @param {Object} param * @param {Object} param.initialState - the initial state object that the reducer will return * @param {Object} param.reducers - a tree of lucid reducers * @param {string[]} param.rootPath - array of strings representing the path to local state in global state * @param {function} param.rootSelector - a top-level selector which takes as input state that has run through every selector in param.selectors * @param {Object} param.selectors - a tree of lucid selectors * @return {Object} redux reducer and connectors */ interface IGetReduxPrimitives { initialState: object; reducers: object; rootPath?: string[]; rootSelector?: (arg0: any) => any; selectors?: object; } export function getReduxPrimitives({ initialState, reducers, rootPath = [], rootSelector = _.identity, selectors, }: IGetReduxPrimitives) { /* 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: object; const reducer = createReduxReducer(reducers, initialState, rootPath); const selector = selectors ? reduceSelectors(selectors) : _.identity; const rootPathSelector = (state: object) => _.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: Funk) => getDispatchTree(reducers, rootPath, dispatch); const devModeMapStateToProps = (rootState: object) => { /* 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: Funk | FunkThunk, rootPath: string[], path: string[] ) { if ((node as FunkThunk).isThunk) { return function thunk(...args: any[]) { return function thunkInner( dispatch: Funk, getState: Funk, ...rest: any[] ) { 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: any[]) { 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: object, rootPath: string[], path: string[] = rootPath ): object { 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: object, rootPath: string[], dispatch: Funk ) { 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; } } /** * Walks the reducer tree and generates a tree of redux reducers, converting the * signature from `(state, payload) => state` to `(state, action) => state` * @param {Object} reducers - a tree of lucid reducers * @param {string[]} path - array of strings representing the path to the reducer * @return {Object} redux reducer tree */ type PayloadReducer = (state: object, payload: any, ...args: any) => any; type ActionReducer = (state: object, action: object, ...args: any) => any; function createReduxReducerTree(reducers: object, path: string[] = []): object { return _.reduce( reducers, (memo, node, key) => { // filter out thunks from the reducer tree if ((node as FunkThunk).isThunk) { return memo; } const currentPath = path.concat(key); return { ...memo, [key]: _.isFunction(node) ? function reduxReducer( state: object, action: { type: any; payload: any; meta: []; } ) { const { type, payload, meta = [] } = action; if (_.isUndefined(state) || type !== currentPath.join('.')) { return state; } return (node as PayloadReducer)(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: object, initialState: object ) { return function reduxReducer(state: any, action: object): object | Funk { if (_.isUndefined(state)) { return initialState; } return _.reduce( reduxReducerTree, (state, node, key) => { return { ...state, ...(_.isFunction(node) ? (node as ActionReducer)(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: object, initialState: object, rootPath: string[] ) { 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: any, dispatch: Funk, path: string[] = [] ): object { return _.reduce( actionCreatorTree, (memo, node, key: string) => ({ ...memo, [key]: _.isFunction(node) ? function boundActionCreator(...args: any) { 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: any[]) { return _.has(_.last(args), 'event') ? _.dropRight(args) : args; }