reactjsreduxreselect

Is reselect needed when I just map the normal state to a component?


I'm new to reselect and I understand the need. I think it's awesome. However, in my case it kind-of seems like it adds a lot of extra code for no reason. Maybe I'm doing it wrong.

Previous Component:

const mapStateToProps = (state) => {
    return {
        day: state.filters.day,
        minDate: state.filters.minDate,
        maxDate: state.filters.maxDate,
    };
};

Now Selectors:

import { createSelector } from 'reselect';

const getDay = state => state.filters.day;
export const makeGetDay = () => createSelector(
    getDay,
    day => day,
);

const getMinDate = state => state.filters.minDate;
export const makeGetMinDate = () => createSelector(
    getMinDate,
    date => date,
);

const getMaxDate = state => state.filters.maxDate;
export const makeGetMaxDate = () => createSelector(
    getMaxDate,
    date => date,
);

Now Component:

const makeMapStateToProps = () => {
    const getDay = makeGetDay();
    const getMinDate = makeGetMinDate();
    const getMaxDate = makeGetMaxDate();
    return state => ({
        day: getDay(state),
        minDate: getMinDate(state),
        maxDate: getMaxDate(state),
    });
};

To clarify, the code works, I just don't understand what Reselect adds in this case..


Solution

  • In the cases you have specified in your question Reselect does not actually add any value.

    The reason is that connect provided by react-redux will do it's own shallow compare of the props provided in your mapStateToProps function to determine if a render is required. In the examples you provided if the values of day, minDate or maxDate do not change you will not be wasting any time with unnecessary renders.

    The real value of Reselect comes in when your selector returns something that is computed.

    To borrow an example from Vlad. Reselect is great for composing selectors so your selectors may look like this:

    export const getDay = state => state.filters.day;
    export const getMinDate = state => state.filters.minDate;
    export const getMaxDate = state => state.filters.maxDate;
    
    export const getIsDateOutOfRange = createSelector(
      getDay,
      getMinDate,
      getMaxDate,
      (day, minDate, maxDate) => day > maxDate || day < minDate
    );
    

    And your mapStateToProps function may look like this:

    const mapStateToProps = state => ({
        isOutOfRange: getIsDateOutOfRange(state)
    });
    

    In this case Reselect is providing a nice syntax for combining selectors and a marginal performance benefit from the fact that getIsDateOutOfRange will only be re-computed if one of it's dependant selectors returns a different value.

    And there in lies the hidden performance benefit of Reselect. If you have a selector that returns a computed array or an object then two identical values returned from the selector will not pass a shallow equality check that Reselect or connect will use for memoization purposes.

    [0, 1] === [0, 1] // false
    

    So for a contrived example:

    export const getDays = state => state.filters.days;
    export const getMinDate = state => state.filters.minDate;
    export const getMaxDate = state => state.filters.maxDate;
    
    export const getDaysWithinRangeNotPerformant = state => {
        const days = getDays(state);
        const minDate = getMinDate(state);
        const maxDate = getMaxDate(state);
        return days.filter(day => day > minDate && day < maxDate);
    };
    
    export const getDaysWithinRangePerformant = createSelector(
        getDays,
        getMinDate,
        getMaxDate,
        (days, minDate, maxDate) =>
            days.filter(day => day > minDate && day < maxDate)
    );
    

    The performance benefit that Reselect unlocks here is two-fold.

    First even with multiple calls to getDaysWithinRangePerformant the possibly expensive filter is only performed if the actual parameters have changed.

    Secondly, and most importantly, every time getDaysWithinRangeNotPerformant is called by connect it will return a new array which means that the shallow compare of the prop in connect will be false and render will be called again even if the actual days themselves have not changed. Because getDaysWithinRangePerformant is memoized by createSelector it will return the exact same array instance if the values have not changed and therefore the shallow compare of props in connect will be true and it will be able to perform it's own optimisations and avoid an unnecessary render.

    And that in my opinion is the big benefit that Reselect provides.