reactjsfunctional-programmingcurryingreselect

Can someone explain how input functions are used in functions in reselect library?


https://github.com/reduxjs/reselect/blob/master/src/index.js#L89

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null
  let lastResult = null
  // 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
  }
}

export function createSelectorCreator(memoize, ...memoizeOptions) {
  return (...funcs) => {
    let recomputations = 0
    const resultFunc = funcs.pop()
    const dependencies = getDependencies(funcs)

    const memoizedResultFunc = memoize(
      function () {
        recomputations++
        // apply arguments instead of spreading for performance.
        return resultFunc.apply(null, arguments)
      },
      ...memoizeOptions
    )
...}
}


export const createSelector = createSelectorCreator(defaultMemoize)

So if I create createSelector(getUsers, (users) => users) for making a simple example. How is it run behind with the codes from above ?

createSelectorCreator(defaultMemoize) is called with getUsers, (users) => users inputs. Now defaultMemoize is also a function that returns a function. How are they all interacting to return the value ?


Solution

  • I think more important to how reselect works is why one should use it. The main reasons are composability and memoization:

    Composability

    Another way of saying this is that you write a selector once and reuse it in other more detailed selectors. Lets say I have a state like this: {data:{people:[person,person ...]} Then I can write a filterPerson like this:

    const selectData = state => state.data;
    const selectDataEntity = createSelector(
      selectData,//reuse selectData
      (_, entity) => entity,
      (data, entity) => data[entity]
    );
    const filterDataEntity = createSelector(
      selectDataEntity,//reuse selectDataEntity
      (a, b, filter) => filter,
      (entities, filter) => entities.filter(filter)
    );
    

    If I move data to state.apiResult then I only need to change selectData, this maximizes the reuse of code and minimizes duplication of implementation.

    Memoization

    Memoization means that when you call a function with the same arguments multiple times the function will only be executed once. Pure functions return the same result given the same arguments no matter how many times they are called or when they are called.

    This means that you don't need to execute the function when you call it again with the same parameters because you already know the result.

    Memoization can be used to not call expensive functions (like filtering a large array). In React memoization is important because pure components will re render if props change:

    const mapStateToProps = state => {
      //already assuming where data is and people is not
      //  a constant
      return {
        USPeople: state.data.people.filter(person=>person.address.countey === US)
      }
    }
    

    Even if state.data.people didn't change the filter function would return a new array every time.

    How it works

    Below is a re write of createSelector with some comments. Removed some code that would safety check parameters and allow you to call createSelector with an array of functions. Please comment if there is anything you have difficulty understanding.

    const memoize = fn => {
      let lastResult,
        //initial last arguments are not going to be the same
        //  as anything you will pass to the function the first time
        lastArguments = [{}];
      return (...currentArgs) => {//returning memoized function
        //check if currently passed arguments are the same as
        //  arguments passed last time
        const sameArgs =
          currentArgs.length === lastArguments.length &&
          lastArguments.reduce(
            (result, lastArg, index) =>
              result && Object.is(lastArg, currentArgs[index]),
            true
          );
        if (sameArgs) {
          //current arguments are the same as the last so just
          //  return the last result and don't execute the function
          return lastResult;
        }
        //current arguments are not the same as last time
        //  or function called for the first time, execute the
        //  function and set the last result
        lastResult = fn.apply(null, currentArgs);
        //set last args to current args
        lastArguments = currentArgs;
        //return result
        return lastResult;
      };
    };
    
    const createSelector = (...functions) => {
      //get the last function by popping it off of the functions
      //  this mutates functions so functions do not have the
      //  last function on it anymore
      //  also memoize the last function
      const lastFunction = memoize(functions.pop());
      //return a selector function
      return (...args) => {
        //execute all the functions (last was already removed)
        const argsForLastFunction = functions.map(fn =>
          fn.apply(null, args)
        );
        //return the result of a call to lastFunction with the
        //  result of the other functions as arguments
        return lastFunction.apply(null, argsForLastFunction);
      };
    };
    //selector to get data from state
    const selectData = state => state.data;
    //select a particular entity from state data
    //  has 2 arguments: state and entity where entity
    //  is a string (like 'people')
    const selectDataEntity = createSelector(
      selectData,
      (_, entity) => entity,
      (data, entity) => data[entity]
    );
    //select an entity from state data and filter it
    //  has 3 arguments: state, entity and filterFunction
    //  entity is string (like 'people') filter is a function like:
    //  person=>person.address.country === US
    const filterDataEntity = createSelector(
      selectDataEntity,
      (a, b, filter) => filter,
      (entities, filter) => entities.filter(filter)
    );
    //some constants
    const US = 'US';
    const PEOPLE = 'people';
    //the state (like state from redux in connect or useSelector)
    const state = {
      data: {
        people: [
          { address: { country: 'US' } },
          { address: { country: 'CA' } },
        ],
      },
    };
    //the filter function to get people from the US
    const filterPeopleUS = person =>
      person.address.country === US;
    //get people from the US first time
    const peopleInUS1 = filterDataEntity(
      state,
      PEOPLE,
      filterPeopleUS
    );
    //get people from the US second time
    const peopleInUS2 = filterDataEntity(
      state,
      PEOPLE,
      filterPeopleUS
    );
    
    console.log('people in the US:', peopleInUS1);
    console.log(
      'first and second time is same:',
      peopleInUS1 === peopleInUS2
    );