4

I tried to boil down the problem into an example as simple as possible:

We have a list of child components, each called NumChoice, each representing a number. NumChoice is wrapped in React.memo. In the parent component, we have an array of booleans, choices, each corresponding to one of the child components NumChoice. At first, all the elements of choices are false. To render the child components, we iterate through choices, and for each choice, generate the corresponding child component NumChoice. We define a function chooseDivisibles in the parent component, using useCallback that is called from each child component NumChoice. chooseDivisibles takes the index of the NumChoice who called it and changes the corresponding element of choices to true. Each NumChoice has a "red" background color if its corresponding element in choices is true, otherwise, its background color is "white".

The complete code is available at: https://codesandbox.io/s/react-rerender-l4e3c?fontsize=14&hidenavigation=1&theme=dark

Wrapping NumChoice in React.memo and chooseDivisibles in useCallback, we expected to only rerender NumChoice components whose corresponding element of choices changes but React re-renders them all. chooseDivisibles is wrapped in useCallback, which lists no dependency other than setChoices. Also, NumChoice is wrapped in React.memo and it should only rerender if the specified props change, but they do not, and changing choices should not have any effect on rerendering NumChoice. If we exclude checking the equality of chooseDivisibles in previous and next props, it works as expected, but I argue that the comparison of previous and next chooseDivisibles should not affect rerendering NumChoice because it is wrapped in useCallbackand does not depend on choices. How can we prevent re-rendering the NumChoice components whose props are not changed?

1man
  • 5,216
  • 7
  • 42
  • 56
  • Before you added the `prevProps.chooseDivisibles === nextProps.chooseDivisiles` everything seemed to be working fine, the profiler wasn't showing components whose props remained the same re-render. – eMontielG Mar 04 '20 at 18:41
  • I think your suggestion does not solve the issue because if we change the equalities to `(prevProps, nextProps) => prevProps === nextProps`, it does rerender all the components. Considering that `chooseDivisibles` is wrapped in `useCallback`, which lists no dependency other than `setChoices`, changing the state should not change `chooseDivisibles` and should not rerender `NumChoice` If you try the same thing without using `map` through an array, this issue does not happen. Please look at the new code that I just saved. – 1man Mar 04 '20 at 18:45
  • 1
    As far as I know, React already (shallow) compares the previous props with the new props object, so doing `prevProps === newProps` is superfluous, and actually I believe it's wrong, since the result will always be false regardless if you do it that way, as objects are pass-by-reference. – eMontielG Mar 04 '20 at 18:56
  • @eMontielG Your answer is right. Thank you. Please post it and I'll accept is as the correct answer. To address your concern, I changed `chooseDivisibles={event => chooseDivisibles(idx)}` to `chooseDivisibles={chooseDivisibles}` and replaced the function call inside `NumChoice` to `onClick={event => props.chooseDivisibles(props.num)}`. Then, I compared `prevProps.num === nextProps.num && prevProps.choice === nextProps.choice && prevProps.chooseDivisibles === nextProps.chooseDivisibles` and it works as expected. – 1man Mar 04 '20 at 19:05
  • By the way, I think I found your issue, you should also be memoizing the component that is looping the choices. – eMontielG Mar 04 '20 at 19:09
  • @eMontielG I disagree. The parent component should definitely change when its state `choices` changes. – 1man Mar 04 '20 at 19:11
  • Not really quite sure but I found [this answer](https://stackoverflow.com/a/60388915/11938059) and tried it myself, and it does stop child components from being highlighted when using the component profiler. [Have a look](https://5j7hy.csb.app/), and the [code](https://codesandbox.io/s/react-how-to-prevent-re-rendering-child-components-in-map-5j7hy). Just check the 'Highlight updates when components render' option. What's weird is that if you record the graph won't show the components being re-rendered regardless. I can't answer since I'm at a loss as well. – eMontielG Mar 04 '20 at 19:21

1 Answers1

1

Ah I see that in NumChoice.js we're also asserting prevProps.chooseDivisibles === nextProps.chooseDivisibles, which is always false, since chooseDivisibles={event => chooseDivisibles(idx)} generates a new function every time

If you remove prevProps.chooseDivisibles === nextProps.chooseDivisibles, it will only re-render the affected ones!

TiHuan
  • 65
  • 9
  • I think your suggestion does not solve the issue because if we change the equalities to `(prevProps, nextProps) => prevProps === nextProps`, it does rerender all the components. Considering that `chooseDivisibles` is wrapped in `useCallback`, which lists no dependency other than `setChoices`, changing the state should not change `chooseDivisibles` and should not rerender `NumChoice` If you try the same thing without using `map` through an array, this issue does not happen. – 1man Mar 04 '20 at 18:44
  • To address your concern, I changed `chooseDivisibles={event => chooseDivisibles(idx)}` to `chooseDivisibles={chooseDivisibles}` and replaced the function call inside `NumChoice` to `onClick={event => props.chooseDivisibles(props.num)}`, but it still rerenders all the `NumChoice` components. – 1man Mar 04 '20 at 19:02
  • Hey @1man Sorry I meant we could just comment out asserting the function. Like so: `(prevProps, nextProps) => prevProps.num === nextProps.num && prevProps.choice === nextProps.choice // prevProps.chooseDivisibles === nextProps.chooseDivisibles <-- comment this out` forked example: https://codesandbox.io/s/react-how-to-prevent-re-rendering-child-components-in-map-j4tr5 – TiHuan Mar 04 '20 at 21:29
  • We found the solution, but we should not comment our `prevProps.chooseDivisibles === nextProps.chooseDivisibles` because `chooseDivisibles` is wrapped in `useCallback`, which prevents changing the function object when `choices` change. So, we don't need to comment out that comparison. However, if we comment it out and for some other reason `chooseDivisibles` changes, we do want to rerender `NumChoice`. – 1man Mar 05 '20 at 00:56
  • Ah that's great to hear! Thanks for the update I just updated the example to separate some concerns in ``, so we could simplify the interface of `` and make its memo function more straight forward https://codesandbox.io/s/react-how-to-prevent-re-rendering-child-components-in-map-j4tr5 Please let me know what you think! – TiHuan Mar 05 '20 at 18:41