All files / util state-management.js

94.34% Statements 50/53
92.68% Branches 38/41
95% Functions 19/20
94.23% Lines 49/52
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169          40x 88x             141x 202x 33x 169x 138x   202x         67x 67x 37x 27x   10x 10x           18x 63x         16x   16x 236x 10x 4x 4x       226x     16x 72x 2x     70x     16x   8x     35x                             2175x 62x                     465x 4x           461x       10x                                   2168x   2168x   1x         1x     2167x   2167x               6x 6x     6x 46x 20x       26x     26x                  
import React, { isValidElement } from 'react';
import _ from 'lodash';
import { logger } from './logger';
 
export function getDeepPaths (obj, path=[]) {
	return _.reduce(obj, (terminalKeys, value, key) => (
		_.isPlainObject(value) ?
			terminalKeys.concat(getDeepPaths(value, path.concat(key))) :
			terminalKeys.concat([path.concat(key)])
	), []);
}
 
export function omitFunctionPropsDeep(obj) {
	return _.reduce(obj, (memo, value, key) => {
		if (_.isPlainObject(value)) {
			memo[key] = omitFunctionPropsDeep(value);
		} else if (!_.isFunction(value)) {
			memo[key] = value;
		}
		return memo;
	}, {});
}
 
export function bindReducerToState(reducerFunction, { getState, setState }, path=[]) {
	let localPath = _.take(path, _.size(path) - 1);
	return _.assign(function (...args) {
		if (_.isEmpty(localPath)) {
			setState(reducerFunction(getState(), ...args));
		} else {
			let localNextState = reducerFunction(_.get(getState(), localPath), ...args);
			setState(_.set(_.clone(getState()), localPath, localNextState));
		}
	}, { path });
}
 
export function bindReducersToState(reducers, { getState, setState }) {
	return _.reduce(getDeepPaths(reducers), (memo, path) => {
		return _.set(memo, path, bindReducerToState(_.get(reducers, path), { getState, setState }, path));
	}, {});
}
 
export function getStatefulPropsContext(reducers, { getState, setState }) {
	const boundReducers = bindReducersToState(reducers, { getState, setState });
 
	const combineFunctionsCustomizer = (objValue, srcValue) => {
		if (_.isFunction(srcValue) && _.isFunction(objValue)) {
			return function (...args) {
				objValue(...args);
				return srcValue(...args);
			};
		}
 
		return safeMerge(objValue, srcValue);
	};
 
	const bindFunctionOverwritesCustomizer = (objValue, srcValue) => {
		if (_.isFunction(srcValue) && _.isFunction(objValue)) {
			return bindReducerToState(srcValue, { getState, setState }, objValue.path);
		}
 
		return safeMerge(objValue, srcValue);
	};
 
	return {
		getPropReplaceReducers(props) {
			return _.mergeWith({}, boundReducers, getState(), props, bindFunctionOverwritesCustomizer);
		},
		getProps(props) {
			return _.mergeWith({}, boundReducers, getState(), props, combineFunctionsCustomizer);
		},
	};
}
 
/**
 * reduceSelectors
 *
 * Generates a root selector from a tree of selectors
 * @param {Object} selectors - a tree of selectors
 * @returns {function} root selector that when called with state, calls each of
 * the selectors in the tree with the state local to that selector
 */
 
export function reduceSelectors(selectors) {
	return function reducedSelector(state) {
		return _.reduce(selectors, (state, selector, key) => ({
			...state,
			[key]: _.isFunction(selector) ?
				selector(state) :
				reduceSelectors(selector)(state[key]),
		}), state);
	};
}
 
export function safeMerge(objValue, srcValue) {
	// don't merge arrays
	if (_.isArray(srcValue) && _.isArray(objValue)) {
		return srcValue;
	}
 
	// guards against traversing react elements which can cause cyclical recursion
	// If we don't have this clause, lodash (as of 4.7.0) will attempt to
	// deeply clone the react children, which is really freaking slow.
	if (isValidElement(srcValue)
		|| (_.isArray(srcValue) && _.some(srcValue, isValidElement))
		|| (_.isArray(srcValue) && _.isUndefined(objValue))
	) {
		return srcValue;
	}
 
}
 
export function buildHybridComponent(baseComponent, {
	replaceEvents = false, // if true, function props replace the existing reducers, else they are invoked *after* state reducer returns
	reducers = _.get(baseComponent, 'definition.statics.reducers', {}),
	selectors = _.get(baseComponent, 'definition.statics.selectors', {}),
} = {}) {
 
	const {
		_isLucidHybridComponent,
		displayName,
		propTypes,
		definition: {
			statics = {},
		} = {},
	} = baseComponent;
 
	if (_isLucidHybridComponent) {
 
		logger.warnOnce(
			displayName,
			`Lucid: you are trying to apply buildHybridComponent to ${displayName}, which is already a hybrid component. Lucid exports hybrid components by default. To access the dumb components, use the -Dumb suffix, e.g. "ComponentDumb"`
		);
 
		return baseComponent;
	}
 
	const selector = reduceSelectors(selectors);
 
	return React.createClass({
		propTypes,
		statics: {
			_isLucidHybridComponent: true,
			...statics,
		},
		displayName,
		getInitialState() {
			const { initialState } = this.props; //initial state overrides
			return _.mergeWith({}, omitFunctionPropsDeep(baseComponent.getDefaultProps()), initialState, omitFunctionPropsDeep(this.props), safeMerge);
		},
		componentWillMount() {
			this.boundContext = getStatefulPropsContext(reducers, {
				getState: () => _.mergeWith({}, omitFunctionPropsDeep(this.state), omitFunctionPropsDeep(this.props), safeMerge),
				setState: (state) => { this.setState(state); },
			});
		},
		render() {
			Iif (replaceEvents) {
				return React.createElement(baseComponent, selector(this.boundContext.getPropReplaceReducers(this.props)), this.props.children);
			}
			return React.createElement(baseComponent, selector(this.boundContext.getProps(this.props)), this.props.children);
		},
	});
}
 
export function buildStatefulComponent(...args) {
	logger.warnOnce('buildHybridComponent-once', 'Lucid: buildStatefulComponent has been renamed to buildHybridComponent.');
	return buildHybridComponent(...args);
}