Search code examples
reduxreact-reduxredux-toolkitmemoizationreact-usememo

How does memoization break on Redux createSelector() across multiple components?


According to the documentation here:

Creating Unique Selector Instances:

There are many cases where a selector function needs to be reused across multiple components. If the components will all be calling the selector with different arguments, it will break memoization - the selector never sees the same arguments multiple times in a row, and thus can never return a cached value.

The statements appear to be contradictory to me. Will memoization break with different or the same arguments?

To understand this I created a demo here:

App.js:

import { useSelector, useDispatch } from "react-redux";
import { incrementX, incrementY, selectSum } from "./reducer";

function Sum({ value }) {
  const state = useSelector((state) => state);
  const sum = selectSum(state, value);
  const dispatch = useDispatch();
  return (
    <div>
      <span>
        ({state.x},{state.y})
      </span>
      <button onClick={() => dispatch(incrementX(1))}>X+1</button>
      <button onClick={() => dispatch(incrementY(1))}>Y+1</button>
      <br />
      <span>sum: {sum}</span>
    </div>
  );
}

export default () => {
  return (
    <div>
      <Sum value={1000} />
      <Sum value={1000} />
    </div>
  );
};

reducer.js:

import { createSlice, createSelector } from "@reduxjs/toolkit";

const initialState = { x: 0, y: 0 };

const counterSlice = createSlice({
  name: "data",
  initialState,
  reducers: {
    // action creators to be auto-generated
    incrementX(state, action) {
      state.x += action.payload;
    },
    incrementY(state, action) {
      state.y += action.payload;
    }
  }
});
export const { incrementX, incrementY } = counterSlice.actions;
export default counterSlice.reducer;

// input selectors
const selectX = (state) => state.x;
const selectY = (state) => state.y;

const selectSum = createSelector(
  [selectX, selectY, (state, n) => n], // notation 1
  (x, y, num) => {
    console.log("output selector called: " + num);
    return x + y + num;
  }
);
export { selectSum };

When I clicked on any of the buttons, the output selector of selectSum logged only once, so the memoization is working correctly. So why will we ever need to use useMemo() or useCallback()?


Solution

  • The standard situation would be multiple components using the same selector, and passing in some unique value. An example might be some kind of an "items filtered by category" component:

    function CategoryItemsList({category}) {
      const filteredItems = useSelector(state => selectItemsForCategory( category));
      // render here
    }
    

    Now imagine that the app has:

      <CategoryItemsList category="a" />
      <CategoryItemsList category="b" />
    

    Those two components are going to keep calling selectItemsForCategory with a different second argument every time ("a" vs "b"). That means that it will never memoize properly, and keep having to recalculate the results every time.

    So, there's two main ways to fix this:

    • Create a unique instance of selectItemsForCategory per component, so that each instance keeps getting the same arguments consistently
    • Use the new maxSize option in Reselect 4.1.x, which lets the selector have a cache size > 1