Jack M.
Jack M.

Reputation: 2031

How to prevent unnecessary renders when checking a checkbox?

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

Answers (1)

buzatto
buzatto

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

Related Questions