2

Is there a way to select a derived array from an array in a Redux store without spurious renders?

My Redux store contains an array of objects.

state = {items: [{id: 1, keys...}, {id: 2, keys...}, {id: 3, keys...}, ...]}

I wrote a selector to return an array of ids.

const selectIds = (state: MyStateType) => {
  const {items} = state;
  let result = [];
  for (let i = 0; i < items.length; i++) {
    result.push(items[I].id);
  }
  return result;
};

I then call this selector using react-redux's useSelector hook, inside a component to render out a list of components.

const MyComponent = () => {
  const ids = useSelector(selectIds);

  return (
    <>
      {ids.map((id) => (
        <IdComponent id={id} key={id} />
      ))}
    </>
  );
};

I am finding that MyComponent is being rendered every call to dispatch which breaks down performance at a higher number of array elements.

I have passed in an equality function to useSelector like so:

import {shallowEqual, useSelector } from "react-redux";

const ids = useSelector(selectIds, (a, b) => {
  if (shallowEqual(a, b)) {
    return true;
  }
  if (a.length !== b.length) {
    return false;
  }
  for (let i = 0; i < a.length; i++) {
    if (a[i].id !== b[i].id) {
      return false;
    }
  }
  return true;
});

But dispatch is called enough times that checking equality becomes expensive with a large amount of array elements.

I have tried using the reselect library as well.

const selectItems = (state: MyStateType) => {
  return state.items;
};

const selectIds = createSelector(
  selectItems,
  (items) => {
    let result = [];
    for (let i = 0; i < items.length; i++) {
      result.push(items[i].id);
    }
    return result;
  }
);

However, every time I modify the properties of one array element in state.items via dispatch, this changes the dependency of selectItems which causes selectIds to recalculate.

What I want is for selectIds to only recompute when the ids of state.items are modified. Is this possible?

2 Answers2

4

I think the best you can do here is to combine reselect with the use of shallowEqual:

import { shallowEqual } from "react-redux";

const selectItems = (state: MyStateType) => state.items;

const selectIds = createSelector(
  selectItems,
  (items) => items.map(item => item.id)
);

const MyComponent = () => {
  const ids = useSelector(selectIds, shallowEqual);

  return (
    <>
      {ids.map((id) => (
        <IdComponent id={id} key={id} />
      ))}
    </>
  );
};

Notes

With the code above:

  • The array of ids will be re-created only if state.items change.
  • The ids variable will have a new reference only if the ids changed.

If this solution is not enough (can't afford the shallowEqual) you can take a look at https://github.com/dai-shi/react-tracked it uses a more precise system to track which part of the state is used (using Proxies: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).

Dharman
  • 30,962
  • 25
  • 85
  • 135
Etienne Dldc
  • 141
  • 5
  • Thank you. The solution for me didn't end up being react-tracked, but another package published by dai-shi called proxy-memoize. https://github.com/dai-shi/proxy-memoize. It looks like the official redux docs also recommend this package for the specific use case I outlined. https://redux.js.org/usage/deriving-data-selectors#proxy-memoize – newimprovement Nov 04 '21 at 07:33
0

Another way of doing this is to memoize the ids array in the selector:

const { createSelector, defaultMemoize } = Reselect;

const selectItems = (state) => {
  return state.items;
};

const selectIds = (() => {
  //memoize the array
  const memArray = defaultMemoize((...ids) => ids);
  return createSelector(selectItems, (items) =>
    memArray(...items.map(({ id }) => id))
  );
})(); //IIFE
//test the code:
const state = {
  items: [{ id: 1 }, { id: 2 }],
};
const result1 = selectIds(state);
const newState = {
  ...state,
  items: state.items.map((item) => ({
    ...item,
    newValue: 88,
  })),
};
const result2 = selectIds(newState);
console.log('are they the same:', result1 === result2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
HMR
  • 37,593
  • 24
  • 91
  • 160