reducer/conditionalReducer.js

import identity   from 'lodash.identity';
import isFunction from 'lodash.isfunction';
import verify     from '../util/verify';

/**
 * Create a higher-order reducer that conditionally executes one of
 * the supplied reducerFns, based on the conditionalFn() return
 * directive.
 * 
 * The **Dev Guide** discusses conditionalReducer() in more detail
 * (see {@tutorial conceptConditional}), and additional examples can
 * be found in {@tutorial conceptJoin} and {@tutorial fullExample}.
 *
 * @param {conditionalReducerCB} conditionalFn - a callback function
 * whose return value determines which reducerFn is executed
 * ... truthy: thenReducerFn(), falsy: elseReducerFn().
 *
 * @param {reducerFn} thenReducerFn - the "wrapped" reducer invoked
 * when conditionalFn returns truthy.
 *
 * @param {reducerFn} [elseReducerFn=identity] - the
 * optional "wrapped" reducer invoked when conditionalFn returns
 * falsy.  DEFAULT: [identity function](https://lodash.com/docs#identity)
 *
 * @param {InitialState} [initialState] - the optional fall-back state
 * value used during the state initialization boot-strap process.
 * 
 * @returns {reducerFn} a newly created reducer function (described above).
 */
export default function conditionalReducer(conditionalFn, thenReducerFn, elseReducerFn=identity, initialState) {

  // validate params
  const check = verify.prefix('AstxReduxUtil.conditionalReducer() parameter violation: ');

  check(conditionalFn,             'conditionalFn argument is required');
  check(isFunction(conditionalFn), 'conditionalFn argument is NOT a function');
  check(thenReducerFn,             'thenReducerFn argument is required');
  check(isFunction(thenReducerFn), 'thenReducerFn argument is NOT a function');
  check(isFunction(elseReducerFn), 'elseReducerFn argument is NOT a function');

  // expose our new higher-order reducer
  // NOTE: For more info on he originalReducerState parameter, refer to the Dev Guide {@tutorial originalReducerState}
  return (state=initialState, action, originalReducerState) => {

    // maintain the originalReducerState as the immutable state
    // at the time of the start of the reduction process
    // ... in support of joinReducers()
    // ... for more info, refer to the Dev Guide {@tutorial originalReducerState}
    if (originalReducerState === undefined) {
      originalReducerState = state;
    }

    // execute either thenReducerFn or elseReducerFn, based on conditionalFn
    return conditionalFn(state, action, originalReducerState)
             ? thenReducerFn(state, action, originalReducerState)
             : elseReducerFn(state, action, originalReducerState);
  };

}



//***
//*** Specification: conditionalReducerCB
//***

/**
 * A callback function (used in {@link conditionalReducer}) whose
 * return value determines which reducerFn is executed.
 *
 * @callback conditionalReducerCB
 *
 * @param {*} state - The current immutable state that is the
 * reduction target.
 *
 * @param {Action} action - The standard redux Action object that
 * drives the reduction process.
 *
 * @param {*} originalReducerState - The immutable state at the time
 * of the start of the reduction process.
 *
 * This is useful in determining whether state has changed within a
 * series of reductions {@link joinReducers} ... because each
 * individual reducer only has visibility of the state within it's own
 * reduction process.
 * 
 * Further information can be found in the {@tutorial
 * originalReducerState} discussion of the Dev Guide.
 * 
 * @returns {truthy} A truthy value indicating which reducerFn is
 * executed ... truthy: thenReducerFn(), falsy: elseReducerFn().
 */