Search code examples
javascriptreactjsreact-reduxreselect

Is there a performance penalty when using closures to implement a memoized selector with Reselect?


I've got a question about selector memoization with Reselect.

As far as I understand the documentation of Reselect, the following implementation is the proposed and correct way to memoize a selector that expects a parameter:

const selectOrdersByCustomer = createSelector(
    [
        state => state.orders,
        (state, customerId) => customerId,
    ],
    (orders, customerId) => {
        return orders.filter(order => order.customerId === customerId);
    }
);
const orders = useSelector(state => selectOrdersByCustomer(state, customerId));

One of my colleagues came up with this approach of the same selector:

const selectOrdersByCustomer = customerId => createSelector(
    state => state.orders,
    orders => {
        return orders.filter(order => order.customerId === customerId);
    }
);
const orders = useSelector(selectOrdersByCustomer(customerId));

(I simplified this, the actual implementation in our application is a fair bit more complicated)

I tried to add console.count('counter'); to the component where this selector is used and it seems like both implementations trigger the same amount of rerenders.

My question: Is there a performance penalty of the second implementation in comparison with the first one?


Solution

  • The second way has a performance issue. Each selector has its own cached result, but a new memoized selector function is created every time the component renders. It will run the output selector multiple times. The cached result will not be used.

    import { createSelector } from 'reselect';
    
    const selectOrdersByCustomer1 = createSelector(
        [(state) => state.orders, (state, customerId) => customerId],
        (orders, customerId) => {
            console.count('selectOrdersByCustomer1 output selector');
            return orders.filter((order) => order.customerId === customerId);
        },
    );
    
    const selectOrdersByCustomer2 = (customerId) =>
        createSelector(
            (state) => state.orders,
            (orders) => {
                console.count('selectOrdersByCustomer2 output selector');
                // @ts-ignore
                return orders.filter((order) => order.customerId === customerId);
            },
        );
    
    const state = { orders: [{ customId: 1 }, { customId: 2 }] };
    const x1 = selectOrdersByCustomer1(state, 1);
    const x2 = selectOrdersByCustomer1(state, 1);
    const x3 = selectOrdersByCustomer1(state, 1);
    console.log(selectOrdersByCustomer1.recomputations());
    console.log(x1 === x2, x1 === x3);
    
    const s1 = selectOrdersByCustomer2(1)(state);
    const s2 = selectOrdersByCustomer2(1)(state);
    const s3 = selectOrdersByCustomer2(1)(state);
    console.log(s1 === s2, s1 === s3);
    

    Logs:

    selectOrdersByCustomer1 output selector: 1
    1
    true true
    selectOrdersByCustomer2 output selector: 1
    selectOrdersByCustomer2 output selector: 2
    selectOrdersByCustomer2 output selector: 3
    false false