reactjsreact-redux

Using useSelector when iterating over a list


Let's say I have a selector like this:

export const selectItemById = (state, id) => state.items[id]

Normally, I would be able to use it in my component like so:

const item = useSelector(state => selectItemById(state, id))

But what if I have a list of items that I want to grab?

const itemIds = [101, 105, 192, 204]
const items = useSelector(state => itemIds.map(id => selectItemById(state, id))

I can't do this because it gives me a warning/error in the console:

Selector unknown returned a different result when called with the same parameters. This can lead to unnecessary rerenders.
Selectors that return a new reference (such as an object or an array) should be memoized: https://redux.js.org/usage/deriving-data-selectors#optimizing-selectors-with-memoization

Is there a better way to iterate over a list and repeatedly call a selector?


Solution

  • Because you are computing and returning a new array from the useSelector hook each time it's called I suspect you just need to pass an equality comparator to the hook to help the useSelector hook check the array elements shallowly.

    See Equality Comparisons and Updates for details.

    import { shallowEqual, useSelector } from 'react-redux';
    
    ...
    
    const itemIds = [101, 105, 192, 204];
    const items = useSelector(
      state => itemIds.map(id => selectItemById(state, id)),
      shallowEqual
    );
    

    I suggest also creating another selector function that consumes the array of ids and computes the result array internally:

    export const selectItemsById =
      (state, itemIds) => itemIds.map(id => state.items[id]);
    
    const itemIds = [101, 105, 192, 204];
    const items = useSelector(
      state => selectItemsById(state, itemIds),
      shallowEqual
    );
    

    Following from the above example, you can also create a memoized selector function using Reselect (re-exported by Redux-Toolkit as well) to further reduce unnecessary re-renders, i.e. if state.items doesn't change then the selectItemsById value doesn't change and the useSelector hook won't trigger the component to re-render:

    import { createSelector } from "reselect"; // or "@reduxjs/toolkit"
    
    const selectItems = state => state.items;
    
    export const selectItemsById = createSelector(
      [
        selectItems,
        (state, itemIds) => itemIds,
      ],
      (items, itemIds) => itemIds.map(id => items[id])
    );
    
    const itemIds = React.useMemo(() => [101, 105, 192, 204], []);
    const items = useSelector(
      state => selectItemsById(state, itemIds),
      shallowEqual
    );