ReactJS: Manage multiple checkbox inputs with useState

All we need is an easy explanation of the problem, so here it is.

I have the following example component that uses multiple checkboxes for choosing what items to remove from a list of objects:

import React, { useState } from "react";
import "./styles.css";

const data = [
  {
    name: "test1",
    result: "pass"
  },
  {
    name: "test2",
    result: "pass"
  },
  {
    name: "test3",
    result: "pass"
  },
  {
    name: "test4",
    result: "pass"
  },
  {
    name: "test5",
    result: "pass"
  }
];

export default function App() {
  const [allChecked, setAllChecked] = useState(false);
  const [isChecked, setIsChecked] = useState({});
  const [formData, setFormData] = useState(data);

  const handleAllCheck = e => {
    setAllChecked(e.target.checked);
  };

  const handleSingleCheck = e => {
    setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
  };

  const onDelete = () => {
    console.log(isChecked);
    const newData = data.filter(
      item => !Object.keys(isChecked).includes(item.name)
    );
    console.log(newData);
    setFormData(newData);
  };

  return (
    <div className="App">
      <div>
        <label>All</label>
        <input
          name="checkall"
          type="checkbox"
          checked={allChecked}
          onChange={handleAllCheck}
        />
        <label />
      </div>
      {formData.map((test, index) => (
        <div key={index}>
          <label>{test.name}</label>
          <input
            type="checkbox"
            name={test.name}
            checked={allChecked ? true : isChecked[test.name]}
            onChange={handleSingleCheck}
          />
        </div>
      ))}
      <button onClick={() => onDelete()}>DELETE</button>
    </div>
  );
}

This is mostly working, except for check all. It seems onChange will not update while using useState. I need to be able to select all the objects or uncheck some to mark for deletion.

Any help is greatly appreciated.

CodeSandbox Example: https://codesandbox.io/s/modest-hodgkin-kryco

UPDATE:

Okay, after some help from Richard Matsen,

Here is a new solution without direct DOM manipulation:

import React, { useState, useEffect } from "react";
import "./styles.css";

const data = [
  {
    name: "test1",
    result: "pass"
  },
  {
    name: "test2",
    result: "pass"
  },
  {
    name: "test3",
    result: "pass"
  },
  {
    name: "test4",
    result: "pass"
  },
  {
    name: "test5",
    result: "pass"
  }
];

export default function App() {
  const [allChecked, setAllChecked] = useState(false);
  const [isChecked, setIsChecked] = useState();
  const [loading, setLoading] = useState(true);
  const [formData, setFormData] = useState(data);

  const handleAllCheck = e => {
    setAllChecked(e.target.checked);
  };

  const handleSingleCheck = e => {
    setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
  };

  const onDelete = () => {
      const itemList = Object.keys(isChecked).map((key:any) => {
        if (isChecked[key] === true) {
          return key
        }
      })
      const result = formData.filter((item:any) => !itemList.includes(item.name))
      console.log(result)
      setFormData(result)
    }

  useEffect(() => {
    if (!loading) {
    setIsChecked(current => {
      const nextIsChecked = {}
      Object.keys(current).forEach(key => {
        nextIsChecked[key] = allChecked;
      })
      return nextIsChecked;
    });
    }
  }, [allChecked, loading]);

  useEffect(() => {
    const initialIsChecked = data.reduce((acc,d) => {
      acc[d.name] = false;
      return acc;
    }, {})
    setIsChecked(initialIsChecked)
    setLoading(false)
  }, [loading])

  return (
    <div className="App">
      <div>
        <label>All</label>
        <input
          name="checkall"
          type="checkbox"
          checked={allChecked}
          onChange={handleAllCheck}
        />
        <label />
      </div>
      {!loading ? formData.map((test, index) => (
      <div key={index}>
        <label>{test.name}</label>
        <input
          type="checkbox"
          name={test.name}
          checked={isChecked[test.name]}
          onChange={handleSingleCheck}
        />
      </div>
      )): null}
      <button onClick={() => onDelete()}>DELETE</button>
    </div>
  );
}

codesandbox of working solution:
https://codesandbox.io/s/happy-rubin-5zfv3

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

The basic problem is checked={allChecked ? true : isChecked[test.name]} stops the unchecking action from happening – once allChecked is true it does not matter what value isChecked[test.name] has, the expression is always going to be true.

You should rely only on isChecked for the value, and treat changing allChecked as a side-effect.

  useEffect(() => {
    setIsChecked(current => {
      const nextIsChecked = {}
      Object.keys(current).forEach(key => {
        nextIsChecked[key] = allChecked;
      })
      return nextIsChecked;
    });
  }, [allChecked]);

  ...

  {formData.map((test, index) => (
    <div key={index}>
      <label>{test.name}</label>
      <input
        type="checkbox"
        name={test.name}
        checked={isChecked[test.name]}
        onChange={handleSingleCheck}
      />
    </div>
  ))}

There’s also this warning cropping up

Warning: A component is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.

So that’s basically saying don’t initialize isChecked to {}, because the input’s checked property is initially undefined. Use this instead,

{
  test1: false,
  test2: false,
  test3: false,
  test4: false,
  test5: false,
}

or this way

const data = { ... }

const initialIsChecked = data.reduce((acc,d) => {
  acc[d.name] = false;
  return acc;
}, {})

export default function App() {
  const [allChecked, setAllChecked] = useState(false);
  const [isChecked, setIsChecked] = useState(initialIsChecked);
...

Method 2

The problem with your code was how you were handling allChecked. I have made some changes to your code and it works now.

const data = [
  {
    name: "test1",
    result: "pass"
  },
  {
    name: "test2",
    result: "pass"
  },
  {
    name: "test3",
    result: "pass"
  },
  {
    name: "test4",
    result: "pass"
  },
  {
    name: "test5",
    result: "pass"
  }
];

function App() {
  const [allChecked, setAllChecked] = useState(false);
  // using an array to store the checked items
  const [isChecked, setIsChecked] = useState([]);
  const [formData, setFormData] = useState(data);

  const handleAllCheck =  e => {
      if (allChecked) {
        setAllChecked(false);
        return setIsChecked([]);
      }
      setAllChecked(true);
      return setIsChecked(formData.map(data => data.name));
    };

  const handleSingleCheck = e => {
    const {name} = e.target;
    if (isChecked.includes(name)) {
      setIsChecked(isChecked.filter(checked_name => checked_name !== name));
      return setAllChecked(false);
    }
    isChecked.push(name);
    setIsChecked([...isChecked]);
    setAllChecked(isChecked.length === formData.length)
  };

  const onDelete = () => {
    const data_copy = [...formData];
    isChecked.forEach( (checkedItem) => {
        let index = formData.findIndex(d => d.name === checkedItem)
        delete data_copy[index]
      }
    )
    setIsChecked([])
    // filtering out the empty elements from the array
    setFormData(data_copy.filter(item => item));
    setAllChecked(isChecked.length && isChecked.length === data.length);
  };

  return (
    <div className="App">
      <form>
        <label>All</label>
        <input
          name="checkall"
          type="checkbox"
          checked={allChecked}
          onChange={handleAllCheck}
        />
        { formData.map((test, index) => (
          <div
          key={index}
          >
            <label>{test.name}</label>
            <input
              type="checkbox"
              name={test.name}
              checked={isChecked.includes(test.name)}
              onChange={handleSingleCheck}
            />
          </div>
          ))
        }
        <label />
      </form>
      <button onClick={onDelete}>DELETE</button>
    </div>
  );
}


Method 3

I think you should merge allChecked and isChecked state vars, because they represent the same thing, but your denormalizing it by creating two different vars! I suggest to keep isChecked, and modify all its entries when you press the allChecked input. Then, you can use a derived var allChecked (defined in your component or by using useMemo hook) to know if all your checks are checked or not.

Method 4

Well, after some time working I came up with:

import React, { useState } from "react";
import "./styles.css";
import { useFormInputs } from "./checkHooks";

const data = [
  {
    name: "test1",
    result: "pass"
  },
  {
    name: "test2",
    result: "pass"
  },
  {
    name: "test3",
    result: "pass"
  },
  {
    name: "test4",
    result: "pass"
  },
  {
    name: "test5",
    result: "pass"
  }
];

export default function App() {
  const [fields, handleFieldChange] = useFormInputs({
    checkedAll: false
  });

  const allcheck = () => {
    const checkdata = document.querySelectorAll(".checkers").length;
    const numChecks = Array.from(new Array(checkdata), (x, i) => i);
    numChecks.map(item => {
      console.log(item);
      async function checkThem() {
        let element = await document.getElementsByClassName("checkers")[item];
        element.click();
      }
      return checkThem();
    });
  };

  return (
    <div className="App">
      <div>
        <label>All</label>
        <input name="checkall" type="checkbox" onChange={allcheck} />
        <label />
      </div>
      {data.map((test, index) => (
        <div key={index}>
          <label>{test.name}</label>
          <input
            className="checkers"
            type="checkbox"
            name={test.name}
            onChange={handleFieldChange}
          />
        </div>
      ))}
    </div>
  );
}

Relevent codesandbox: https://codesandbox.io/s/admiring-waterfall-0vupo

Any suggestions welcomed. Also, thanks for the help guys!

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply