BlogReactJS

React checkbox onChange not firing/updating on the first click

Written by Codemzy on August 17th, 2023

If your React checkbox isn't working, or it takes two clicks to change the state, here are some common bugs and easy fixes!

In my last post, we looked at controlled and uncontrolled checkboxes in React and settled on controlled checkboxes for most use cases.

But even if you do everything right with your checkbox you might find the checkbox just doesn't work. You might run into issues like:

  • the checkbox not checking
  • the checkbox state not changing on the first click (e.g. you need to click it twice to work)

Annoying!

I've experienced these issues, and I've found a few common culprits that are nearly always to blame.

The event.preventDefault() on click bug

I'm starting with this one because it's often to blame for the checkbox only working every other time.

You might use this as e.preventDefault() or event.preventDefault() depending on if you are passing the event as e or event to your click handler!

Let's start with an example:

function Checkbox() {
  // state
  const [checked, setChecked] = React.useState(false);
  
  // checkbox click handler
  function handleClick(e) {
    e.preventDefault();
    setChecked(!checked);
  };
  
  return (
    <div>
      <input type="checkbox" id="bug-one" name="bug-one" checked={checked} onChange={handleClick} />
      <label for="bug-one">Bug 1 - Two Clicks</label>
    </div>
  );
};

Ok, so what's the problem?

Well, if you use this code, you will notice that the checkbox doesn't work when you click it the first time. You have to click it twice!

<div id="app"></div>
function Checkbox() {
  // state
  const [checked, setChecked] = React.useState(false);
  // checkbox click handler
  function handleClick(e) {
    e.preventDefault();
    setChecked(!checked);
  };
  // checkbox render
  return (
    <div className="p-10 flex justify-center items-center h-screen">
      <input type="checkbox" id="bug-one" name="bug-one" checked={checked} onChange={handleClick} />
      <label for="bug-one" className="ml-2 font-bold">Bug 1 - Two Clicks</label>
    </div>
  );
};
// render component
ReactDOM.render(
  <Checkbox />,
  document.getElementById('app')
);

To fix the problem, remove e.preventDefault() from the click handler.

// 🐛 from this
function handleClick(e) {
  e.preventDefault(); // 🐛
  setChecked(!checked);
};

// ✅ to this
function handleClick() {
  setChecked(!checked);
};

Tada! It's fixed!

<div id="app"></div>
function Checkbox() {
  // state
  const [checked, setChecked] = React.useState(false);
  // checkbox click handler
  function handleClick() {
    setChecked(!checked);
  };
  // checkbox render
  return (
    <div className="p-10 flex justify-center items-center h-screen">
      <input type="checkbox" id="bug-one" name="bug-one" checked={checked} onChange={handleClick} />
      <label for="bug-one" className="ml-2 font-bold">Bug 1 - Fixed!</label>
    </div>
  );
};
// render component
ReactDOM.render(
  <Checkbox />,
  document.getElementById('app')
);

The event.preventDefault() on parent bug

Ok, if your checkbox still doesn't work, it's worth mentioning that event.preventDefault() might not even be on your checkbox - but it can still be the problem!

Time for another example.

function Form() {
  return (
    <form onClick={(e) => e.preventDefault()}>
      <Checkbox />
    </form>
  )
};

Now we have a form element wrapping our working Checkbox component. And this time, we have e.preventDefault() on the form. And that stops our checkbox from working.

Let me show you:

<div id="app"></div>
// checkbox component
function Checkbox() {
  // state
  const [checked, setChecked] = React.useState(false);
  // checkbox click handler
  function handleClick() {
    setChecked(!checked);
  };
  // render checkbox
  return (
    <div className="p-10 flex justify-center items-center h-screen">
      <input type="checkbox" id="bug-two" name="bug-two" checked={checked} onChange={handleClick} />
      <label for="bug-two" className="ml-2 font-bold">Bug 2 - Doesn't Work</label>
    </div>
  );
};
// form component
function Form() {
  return (
    <form onClick={(e) => e.preventDefault()}>
      <Checkbox />
    </form>
  )
};
// render react
ReactDOM.render(
  <Form />,
  document.getElementById('app')
);

It's easy to see in this example, but the e.preventDefault() might not be on the form, or even the direct parent of the checkbox. When your checkboxes are children of many components, that e.preventDefault() could be hiding somewhere up the tree.

function Form() {
  return (
    <div onClick={(e) => e.preventDefault()}>
      <form>
        <Checkbox />
      </form>
    </div>
  )
};

To fix this bug, remove the e.preventDefault - wherever it is.

<div id="app"></div>
// checkbox component
function Checkbox() {
  // state
  const [checked, setChecked] = React.useState(false);
  // checkbox click handler
  function handleClick() {
    setChecked(!checked);
  };
  // render checkbox
  return (
    <div className="p-10 flex justify-center items-center h-screen">
      <input type="checkbox" id="bug-two" name="bug-two" checked={checked} onChange={handleClick} />
      <label for="bug-two" className="ml-2 font-bold">Bug 2 - Fixed!</label>
    </div>
  );
};
// form component
function Form() {
  return (
    <form>
      <Checkbox />
    </form>
  )
};
// render react
ReactDOM.render(
  <Form />,
  document.getElementById('app')
);

You might have that e.preventDefault() for some other reason. For example, your form is in a dropdown and you don't want it close when you click the checkbox or whatever.

Instead of relying on e.preventDefault() you can use e.stopPropagation() instead, which stops the click bubbling up any further to other parents.

function Form() {
  return (
    <form onClick={(e) => e.stopPropagation()}>
      <Checkbox />
    </form>
  );
};

The defaultChecked not working bug

Passing defaultChecked to your checkbox won't work in a controlled React component, because the checked state is in control.

function Checkbox({ defaultChecked }) {
  // state
  const [checked, setChecked] = React.useState(defaultChecked);

  // checkbox click handler
  function handleClick() {
    setChecked(!checked);
  };

  // will be unchecked even though defaultChecked is true
  return (
    <div>
      <input type="checkbox" id="bug-three" name="bug-three" defaultChecked={true} checked={checked} onChange={handleClick} />
      <label for="bug-three">Bug 3 - No Default Checked</label>
    </div>
  );
};

To fix this bug, instead of passing defaultChecked to the checkbox input, pass it as the initial value of the checked state. Like this:

// true by default
const [checked, setChecked] = React.useState(true);

// or as a prop
function Checkbox({ defaultChecked }) {
  // state
  const [checked, setChecked] = React.useState(defaultChecked);

  //...
};

The uncontrolled to controlled component bug

If you use the above code (with the defaultChecked prop) and don't pass any value, you might get an error in the console like "Warning: A component is changing an uncontrolled input to be controlled".

For example, if you use the checkbox component like <Checkbox /> instead of <Checkbox defaultChecked={true} />.

So why the error?

Well, if you don't pass any value then defaultChecked will be undefined. And an undefined prop doesn't get passed to the element, so it's got no checked prop initially => which makes it an uncontrolled checkbox.

Then, when you click the checkbox, our onClick handler kicks in, giving our checked state a value of true or false, which does get passed to the input element, making it a controlled checkbox.

The checkbox has changed from an uncontrolled input to a controlled input - hence the error.

To fix this, give your defaultChecked prop a default value, so even if you don't pass the defaultChecked prop, it has an initial value of true or false.

// checkbox component
function Checkbox({ defaultChecked = false }) {
  // state
  const [checked, setChecked] = React.useState(defaultChecked);
  // checkbox click handler
  function handleClick() {
    setChecked(!checked);
  };
  // render checkbox
  return (
    <div>
      <input type="checkbox" id="bug-four" name="bug-four" checked={checked} onChange={handleClick} />
      <label for="bug-four">Bug 4 - uncontrolled to controlled</label>
    </div>
  );
};

Hopefully, these code examples help you figure out why your checkbox isn't updating - I'd put money on those e.preventDefaults() being the problem 99% of the time!