4

I notice that some of my components are rerendering when I wouldn't expect them to. Lets say I have a <Header /> component. <Header /> calls useSelector, and grabs a slice of state. As an example, my whole redux store may look like this:

{
  header: "header slice",
  body: "body slice"
}

<Header /> grabs what it needs with use selector:

const Header = () => {
  const headerSlice = useSelector(state => state.header);
  return <div>{headerSlice}</div>;
}

Elsewhere in the application, something causes the body slice to update. When this happen, I see that <Header /> rerenders. This is not what I would expect. Why might this be happening?

For more detail, I was able to track this down by running the devtools profiler while triggering the action that updates state.body. I see that <Header /> rerenders, and my profiler tells me its due to "Hooks 4, 9, 14, 19, and 24" changed.

enter image description here

When I look at that component's hooks in devtools, I see that all these hooks seem to be part of a Selector -> SyncExternalStoreWithSelector:

enter image description here

But there's not much information here to tell me which selector or was, what the previous value was, etc. When I remove these selectors, the unnecessary rerenders stop. But the values that this component consumes are not the values that are updating. I have tried wrapping the component in React.memo, and it makes no difference. I have not experienced this problem before with redux. Usually store updates only cause rerenders in those components that useSelect updated values.

I am using react 18.2.0, redux 4.2.0, react-redux 8.0.2, and webpack 5.

Seth Lutske
  • 9,154
  • 5
  • 29
  • 78

1 Answers1

3

The first answer here is to use the Redux DevTools and look at what state is changing in the Redux store after each dispatch. Then, compare the state changes vs what values this component is selecting.

The next step is to look at the actual selectors in this component file. Are any of them accidentally returning new references? A common example might be state => state.todos.map() or .filter() or similar, which return new arrays. If those aren't memoized, those will cause unnecessary re-renders even if the original data didn't change.

markerikson
  • 63,178
  • 10
  • 141
  • 157
  • The second part was the issue. I was chaining a `.map` statement directly from `useSelector`. Not exactlty as you said, I thought I was being clever by doing `const thing = useSelector(state => state.some.array).map()`. Fixing this fixes my rerender issue, but frankly, I'm not sure *why*, especially because my `.map` was *outside* the `useSelector` call, not inside it. I've asked a [follow-up question](https://stackoverflow.com/questions/73054937/why-does-the-following-use-of-useselector-break-referential-equality). – Seth Lutske Jul 20 '22 at 16:14
  • Huh. That _should_ be just fine! It's only when you do that _inside_ the `useSelector` that there can be problems. – markerikson Jul 22 '22 at 06:42