Search code examples
javascriptreselect

How 'createSelector' is accepting input parameter in 'reselect' library?


I have taken the following code from the reselect library.

When subtotalSelector is invoked with exampleState, it will invoke the function createSelector that accepts the input parameter exampleState.

My question is about how createSelector is accepting exampleState and the other functions consuming it? There is some implicit injection of the parameter happening which I don't understand.

import { createSelector } from 'reselect'

const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent

const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((acc, item) => acc + item.value, 0)
)

const taxSelector = createSelector(
  subtotalSelector,
  taxPercentSelector,
  (subtotal, taxPercent) => subtotal * (taxPercent / 100)
)

export const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) => ({ total: subtotal + tax })
)

let exampleState = {
  shop: {
    taxPercent: 8,
    items: [
      { name: 'apple', value: 1.20 },
      { name: 'orange', value: 0.95 },
    ]
  }
}

console.log(subtotalSelector(exampleState)) // 2.15

The subtotalSelector is little more explainable by replacing the input parameters.

subtotalSelector = createSelector(
  state => state.shop.items,
  items => items.reduce((acc, item) => acc + item.value, 0)
)

subtotalSelector({
  shop: {
    taxPercent: 8,
    items: [
      { name: 'apple', value: 1.20 },
      { name: 'orange', value: 0.95 },
    ]
  }
});

Solution

  • I did not find a better way to dissect this reselect library code, but to add console.log inside the functions. The following code is copied from the reselect library itself. The JSBin version can be found here.

    At the bottom of the console output, you can see that the exampleState is really the arguments variable used in the code. It is a JavaScript construct.

    function defaultEqualityCheck(a, b) {
      return a === b
    }
    
    function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
      if (prev === null || next === null || prev.length !== next.length) {
        return false
      }
    
      // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
      const length = prev.length
      for (let i = 0; i < length; i++) {
        if (!equalityCheck(prev[i], next[i])) {
          return false
        }
      }
    
      return true
    }
    
    function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
      
      let lastArgs = null
      let lastResult = null
      
      console.log("Entering defaultMemoize");
      
      console.log("###INPUT### defaultMemoize argument func type: " + typeof func);
      
      // we reference arguments instead of spreading them for performance reasons
      return function () {
        
        if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
          
          // apply arguments instead of spreading for performance.
          lastResult = func.apply(null, arguments)
        }
    
        lastArgs = arguments
        
        return lastResult
      }
    }
    
    function getDependencies(funcs) {
      const dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs
    
      if (!dependencies.every(dep => typeof dep === 'function')) {
        const dependencyTypes = dependencies.map(
          dep => typeof dep
        ).join(', ')
        throw new Error(
          'Selector creators expect all input-selectors to be functions, ' +
          `instead received the following types: [${dependencyTypes}]`
        )
      }
    
      return dependencies
    }
    
    function createSelectorCreator(memoize, ...memoizeOptions) {
      
      console.log("Entering createSelectorCreator");
      
      console.log("#INPUT# argument memoize name: " + memoize.name);
    
      console.log("#INPUT# argument memoize options: ");
      
      console.log(memoizeOptions);
    
      return (...funcs) => {
        
        let recomputations = 0
        
        const resultFunc = funcs.pop()
        const dependencies = getDependencies(funcs)
    
        console.log("##INPUT## argument funcs: ");
        console.log(resultFunc);
        
        const memoizedResultFunc = memoize(
          function () {
            recomputations++
            
            // apply arguments instead of spreading for performance.
            return resultFunc.apply(null, arguments)
          },
          ...memoizeOptions
        )
        
        console.log("memoizedResultFunc: " + typeof memoizedResultFunc);
    
        // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
        const selector = defaultMemoize(function () {
          const params = []
          const length = dependencies.length
    
          if (arguments != null)
          {
            console.log("***INPUT*** arguments: ");
            console.log(arguments);
          }
          
          for (let i = 0; i < length; i++) {
            // apply arguments instead of spreading and mutate a local list of params for performance.
            params.push(dependencies[i].apply(null, arguments))
          }
    
          // apply arguments instead of spreading for performance.
          return memoizedResultFunc.apply(null, params)
        })
    
        selector.resultFunc = resultFunc
        selector.recomputations = () => recomputations
        selector.resetRecomputations = () => recomputations = 0
        
        return selector
      }
    }
    
    const createSelector = createSelectorCreator(defaultMemoize)
    
    function createStructuredSelector(selectors, selectorCreator = createSelector) {
      if (typeof selectors !== 'object') {
        throw new Error(
          'createStructuredSelector expects first argument to be an object ' +
          `where each property is a selector, instead received a ${typeof selectors}`
        )
      }
      const objectKeys = Object.keys(selectors)
      return selectorCreator(
        objectKeys.map(key => selectors[key]),
        (...values) => {
          return values.reduce((composition, value, index) => {
            composition[objectKeys[index]] = value
            return composition
          }, {})
        }
      )
    }
    
    const shopItemsSelector = state => state.shop.items
    const taxPercentSelector = state => state.shop.taxPercent
    
    const subtotalSelector = createSelector(
      shopItemsSelector,
      items => items.reduce((acc, item) => acc + item.value, 0)
    )
    
    const taxSelector = createSelector(
      subtotalSelector,
      taxPercentSelector,
      (subtotal, taxPercent) => subtotal * (taxPercent / 100)
    )
    
    const totalSelector = createSelector(
      subtotalSelector,
      taxSelector,
      (subtotal, tax) => ({ total: subtotal + tax })
    )
    
    let exampleState = {
      shop: {
        taxPercent: 8,
        items: [
          { name: 'apple', value: 1.20 },
          { name: 'orange', value: 0.95 },
        ]
      }
    }
    
    console.log(subtotalSelector(exampleState))// 2.15
    //console.log(taxSelector(exampleState))// 0.172
    //console.log(totalSelector(exampleState))// { total: 2.322 }

    The following code shows an example of functional composition similar to the above.

    function firstFunction() {
      console.log(arguments);
    }
    
    function secondFunction() {
      console.log(arguments);
    }
    
    function thirdFunction() {
      console.log(arguments);
    }
    
    function fourthFunction() {
      console.log(arguments);
      
      return function() {
        return function(x) { console.log(arguments); };
      }
    }
    
    const higherOrderFunction = fourthFunction(thirdFunction);
    
    console.log("High Order Function");
    console.log(higherOrderFunction);
    
    const highestOrderFunction = higherOrderFunction(firstFunction, secondFunction);
    
    console.log("Highest Order Function");
    console.log(highestOrderFunction);
    
    highestOrderFunction(10);