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 ?
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
);