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!