I am trying to render multiple buttons in a parent component that manages all child states centrally. This means that the parent stores e.g. the click state, the disabled state for each button in his own state using useState
and passes it as props to the childs. Additionally the onClick
function is also defined inside of the parent component and is passed down to each child. At the moment I am doing this like the following:
const [isSelected, setIsSelected] = useState(Array(49).fill(false));
...
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
...
(In the render function:)
return isSelected.map((isFieldSelected, key) => {
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
})
To try to prevent the child component from rerendering I am using...
useCallback
to make react see that the onClick function always stays the sameReact.Fragment
to make react find a component again because otherwise a child would not have a unique id or sth similarThe child component is exported as:
export default React.memo(TheChildComponent, compareEquality)
with
const compareEquality = (prev, next) => {
console.log(prev, next);
return prev.isSelected === next.isSelected;
}
Somehow the log line in compareEquality
is never executed and therefore I know that compareEquality
is never executed. I don't know why this is happening either.
I have checked all blogs, previous Stackoverflow questions etc. but could not yet find a way to prevent the child components from being rerendered every time that at least one component executes the onClick
function and by doing that updated the isSelected
state.
I would be very happy if someone could point me in the right direction or explain where my problem is coming from.
Thanks in advance!
This code will actually generate a new onClick
function every render, because useCallback
isn't given a deps array:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
});
The following should only create one onClick
function and re-use it throughout all renders:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
Combined with vanilla React.memo
, this should then prevent the children from re-rendering except when isSelected
changes. (Your second argument to React.memo
should have also fixed this -- I'm not sure why that didn't work.)
As a side note, you can simplify this code:
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
to the following:
<TheChildComponent key={key}
isSelected={isFieldSelected}
onClick={onClick}
/>
(assuming you indeed only need a single component in the body of the map
).
Turns out the only problem was neither useCallback
, useMemo
or anything similar.
In the render function of the parent component I did not directly use
return isSelected.map(...)
I included that part from a seperate, very simple component like this:
const Fields = () => {
return isSelected.map((isFieldSelected, i) => (
<TheChildComponent
key={i}
isSelected={isFieldSelected}
onClick={onClick}
/>
));
};
That is where my problem was. When moving the code from the seperate component Fields
into the return statement of the parent component the rerendering error vanished.
Still, thanks for the help.