Reputation: 2031
I'm trying to create performant checkbox tree component. I have a parent level state to hold list of checked checkbox IDs =>
const [selected, setSelected] = useState([]);
and when toggling a checkbox its ID is added or removed to/from that array. I'm passing boolean to each checkbox which controls the checked state =>
checked={selected.includes(hole.id)}
Checkbox -input is separated to a own CheckboxNode component.
When not using React.memo
for the CheckboxNode component I can always see each checkbox from the same parent triggering console.log()
even only one is clicked/toggled
When using React.memo
with following check I see 1-3 renders when toggling the checkboxes =>
const areEqual = (prev, next) => prev.checked === next.checked;
Also the visual states changes really peculiarly and the component feel really buggy.
How could I achieve great performance and getting rid of extra renders in a setup like this? I added the code here so anyone can take a better look: https://codesandbox.io/s/shy-frog-4wjrg?file=/src/CheckboxNode.js
Upvotes: 2
Views: 249
Reputation: 10382
A memoized function with useCallback
it will lead to buggy behaviors if you reference a given state.
This happens because you'll keep a stale reference from that state.
A solution is to call any setState
with a callback function instead. The callback will always pass the current state reference, leading to the expected behavior.
const handleToggleParent = useCallback(
(site) => {
// if you pass a function to setSelected, selected will be always be the correct reference
setSelected((selected) => {
let copyOfOriginal = [...selected];
const parentChecked = copyOfOriginal.includes(site.id);
if (parentChecked) {
// Uncheck parent
copyOfOriginal = copyOfOriginal.filter((id) => id !== site.id);
} else {
// Check parent
copyOfOriginal.push(site.id);
}
for (const hole of site.Holes) {
if (parentChecked) {
// Uncheck all childs
copyOfOriginal = copyOfOriginal.filter((id) => id !== hole.id);
} else {
// Check all childs
copyOfOriginal.push(hole.id);
}
}
return copyOfOriginal;
});
},
[setSelected]
);
const handleToggleChild = useCallback(
(hole, site) => {
// if you pass a function to setSelected, selected will be always be the correct reference
setSelected((selected) => {
let copyOfOriginal = [...selected];
if (copyOfOriginal.includes(hole.id)) {
copyOfOriginal = copyOfOriginal.filter((id) => id !== hole.id);
// also remove parent checked when any child is not checked
copyOfOriginal = copyOfOriginal.filter((id) => id !== site.id);
} else {
copyOfOriginal.push(hole.id);
// also check parent if all child holes are checked
if (site) {
if (site.Holes.every((hole) => copyOfOriginal.includes(hole.id))) {
copyOfOriginal.push(site.id);
}
}
}
return copyOfOriginal;
});
},
[setSelected]
);
Upvotes: 2