How do you use `reselect` to memoize an array?

phtrivier picture phtrivier · Jun 1, 2017 · Viewed 8.3k times · Source

Suppose I have a redux store with this state structure:

{
  items: {
    "id1" : {
      foo: "foo1",
      bar: "bar1"
    },
    "id2": {
      foo: "foo2",
      bar: "bar2"
    } 
  }
}

This store evolves by receiving full new values of items:

const reduceItems = function(items = {}, action) {
  if (action.type === 'RECEIVE_ITEM') {
    return {
      ...items,
      [action.payload.id]: action.payload,
    };
  }
  return items;
};

I want to display a Root view that renders a list of SubItem views, that only extract a part of the state. For example the SubItem view only cares about the foos, and should get it:

function SubItem({ id, foo }) {
  return <div key={id}>{foo}</div>
}

Since I only care about "subpart" of the states, that's what I want to pass to a "dumb" Root view:

const Root = function({ subitems }) {
  // subitems[0] => { id: 'id1', foo: "foo1" }
  // subitems[1] => { id; 'id2', foo : "foo2" }
  const children = subitems.map(SubItem);
  return <div>{children}</div>;
};

I can easily connect this component to subscribe to changes in the state:

function mapStatesToProps(state) {    
 return {
    subitems: xxxSelectSubItems(state)
 }
}
return connect(mapStatesToProps)(Root)

My fundamental problem is what happens when the part of the state that I don't care about (bar) changes. Or even, when I receive a new value of an item, where neither foo nor bar has changed:

setInterval(() => {
    store.dispatch({
      type: 'RECEIVE_ITEM',
      payload: {
        id: 'id1',
        foo: 'foo1',
        bar: 'bar1',
      },
    });
  }, 1000);

If I use the "naive" selector implementation:

// naive version
function toSubItem(id, item) {
  const foo = item.foo;
  return { id, foo };
}

function dumbSelectSubItems(state) {
  const ids = Object.keys(state.items);
  return ids.map(id => {
    const item = state.items[id];
    return toSubItem(id, item);
  });
}

Then the list is a completely new object at every called, and my component gets rendered everytime, for nothing.

Of course, if I use a 'constant' selector, that always return the same list, since the connected component is pure, it is re-renderered (but that's just to illustrate connected components are pure):

// fully pure implementation
const SUBITEMS = [
  {
    id: 'id0',
    foo: 'foo0',
  },
];
function constSelectSubItems(state) {
  return SUBITEMS;
}

Now this gets a bit tricky if I use an "almostConst" version where the List changes, but contains the same element.

const SUBITEM = {
  id: 'id0',
  foo: 'foo0',
};
function almostConstSelectSubItems(state) {
  return [SUBITEM];
}

Now, predictably, since the list is different, even though the item inside is the same, the component gets rerendered every second.

This is where I though 'reselect' could help, but I'm wondering if I am not missing the point entirely. I can get reselect to behave using this:

const reselectSelectIds = (state, props) => Object.keys(state.items);
const reselectSelectItems = (state, props) => state.items;
const reselectSelectSubItems = createSelector([reSelectIds, reSelectItems], (ids, items) => {
  return ids.map(id => toSubItem(id, items));
});

But then it behaves exactly like the naive version.

So:

  • is it pointless to try to memoize an array ?
  • can reselect handle this ?
  • should I change the organisation of the state ?
  • should I just implement shouldComponentUpdate on the Root, using a "deepEqual" test ?
  • should I give up on Root being a connected component, and make each LeafItems be connected components themselves ?
  • could immutable.js help ?
  • is it actually not an issue, because React is smart and will not repaint anything once the virtual-dom is computed ?

It's possible what I'm trying to do his meaningless, and hides an issue in my redux store, so feel free to state obvious errors.

Answer

markerikson picture markerikson · Jun 1, 2017

You're definitely right about the new array references causing re-renders, and sort of on the right track with your selectors, but you do need to change your approach some.

Rather than having a selector that immediately returns Object.keys(state.item), you need to deal with the object itself:

const selectItems = state => state.items;

const selectSubItems = createSelector(
    selectItems,
    (items) => {
        const ids = Object.keys(items);
        return ids.map(id => toSubItem(id, items));
    }
);

That way, the array will only get recalculated when the state.items object is replaced.

Beyond that, yes, you may also want to look at connecting your individual list item components so that each one looks up its own data by ID. See my blog post Practical Redux, Part 6: Connected Lists, Forms, and Performance for examples. I also have a bunch of related articles in the Redux Techniques#Selectors and Normalization and Performance#Redux Performance sections of my React/Redux links list.