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

Viewed 10408

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.

1 Answers
Related