2

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?

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
frehder
  • 88
  • 1
  • 7

2 Answers2

1

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
Lin Du
  • 88,126
  • 95
  • 281
  • 483
0

selectOrdersByCustomer(customerId) Creates a new function every time it is called but still does not need to be a problem if you have a pure component that only re renders when customerId changes:

const PureComponent = React.memo(function Component({ customerId }) {
  const orders = useSelector(selectOrdersByCustomer(customerId))
})

Another way to memoize it is with useMemo:

function Component({ customerId }) {//not pure/memoized component
  const selectMyOrders = useMemo(//memoize the selector based on customerId
    () => selectOrdersByCustomer(customerId),
    [customerId]
  )
  const orders = useSelector(selectMyOrders)
}

Usually your application is wrapped in <React.StrictMode> and in development mode the useMemo may not behave like you expect it to and creates the selector even though the customerId did not change. This is because StrictMode will try to detect memory leaks and calls hooks multiple times in development.

HMR
  • 37,593
  • 24
  • 91
  • 160