Search code examples
javascriptreactjsdom-eventspreventdefaulthtml-input

react checkbox: event.preventDefault() breaks onChange function - why?


I have just found a stray event.preventDefault() that was breaking my checkboxes' onChange handler:

import { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      accepted: false
    }
  }

  changeChecked = (event) => {
    this.setState((state) => ({
      accepted : !state.accepted
    }));
    event.preventDefault(); // <- this very bad
  }

  render() {
    return (
      <input
        type="checkbox" 
        onChange={this.changeChecked}
        checked={this.state.accepted}
      />
    );
  }
}

export default App;

The resulting behaviour is a correctly updated state on first click, but the checkbox only changes to its 'checked' appearance on the next rerender, eg. a second click.

Why is that? Isn't it the point of controlled components to work independently from browser events?

Someone explaining this to me would definitely ease the pain of hours spent boiling down my complex use case. Thank you!

Update: Here's a quick Codepen example demonstrating the odd behaviour. I included an 'unprevented checkbox' and one with a prevented onClick event as comparison. Notice how the one with prevented onChange switches its appearance to its actual state as soon as I click a different checkbox.


Solution

  • Checkboxes behave a little differently. As you likely know, the typical use case for preventDefault() is the onSubmit() function of a form where you will do your own AJAX call, and thus want to prevent the default form submission. But with checkboxe (and most inputs) there's a little more involved.

    Checked attribute

    Per MDN, the checked attribute is "A Boolean attribute indicating whether or not this checkbox is checked by default (when the page loads). It does not indicate whether this checkbox is currently checked: if the checkbox’s state is changed, this content attribute does not reflect the change." In a funny way then, there's a disconnect between the checked attribute, and whether or not the state of the input is checked or not. When it comes to React, on each rerender, the checked attribute will reflect the current state, but it's still only a default value in the sense that the input has been newly rendered, and not manipulated since the state last changed.

    Native eventListener for <input type="checkbox" />

    Also, without getting too off track, the event that natively changes the state on a checkbox input is actually the click event, not the change event. If you were to mess with the listeners and the values of a checkbox input in your browser's js console, you'd see you could manipulate it in a way that the checkbox isn't checked, but the values of the node say otherwise.

    Possible solution

    Based on the above, in this case you don't want to prevent the default behavior, because the default behavior on the change event doesn't conflict with what you want to do (and for some reason preventing it causes problems). In fact, in the Mozilla docs examples, you'll notice they don't use preventDefault() when updating state on controlled components. I wish I had a better understanding of exactly why adding preventDefault() to change handlers for inputs causes problems like this, but hopefully these few little tidbits give some more clarity.