React: event.preventDefault in click handler does not prevent onChange from being called

Created on 17 Feb 2017  ·  6Comments  ·  Source: facebook/react

Do you want to request a feature or report a bug?

Bug!

What is the current behavior?

When rendering an input element of type checkbox with an onClick and onChange handler, onChange is still called even though event.preventDefault() is called in the onClick handler.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: https://jsfiddle.net/reactjs/69z2wepo/).

http://jsfiddle.net/rf3w7apc/

What is the expected behavior?

Calling event.preventDefault in the onClick handler should prevent the default action from occurring (or undo its effect), which is to update the value of the input element. This should stop any change event listener from being invoked. See https://jsfiddle.net/L1eskzsq/ for expected behavior

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

Tested using a build from master, macOS 10.12.2, verified in:

  • Chrome 56.0.2924.87 (64-bit)
  • Firefox 51.0.1 (64-bit)

Safari 10.0.2 calls the change event listener in both cases.

DOM Bug

Most helpful comment

Is this related? preventDefault called in a checkbox onChange causes React and the browser state to go out of sync: https://codesandbox.io/s/0qpr936xkv

All 6 comments

The change event is being created by the ChangeEventPlugin as a response to topChange, before your onClick handler is even called (so before the preventDefault call), here: https://github.com/facebook/react/blob/master/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js#L338

This plugin is trying to figure out if the value of the checkbox will change, and if yes it will queue the change event. It uses the inputValueTracking functionality to compare the previous value with the next value to make this decision, here: https://github.com/facebook/react/blob/master/src/renderers/dom/shared/inputValueTracking.js#L154

The nextValue is take directly from the DOM like this: value = isCheckable(node) ? '' + node.checked : node.value; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/inputValueTracking.js#L56

The problem is that the node.checked value is temporarily true at this point, so after the first click it is:

lastValue == false (correct)
nextValue == true (incorrect)

and on the next click on the checkbox, the change event is no longer being fired, because the values are:

lastValue == true (incorrect)
nextValue == true (incorrect)

Overall the problem seems to be that React is asking the DOM what the checked value of the node is, before the preventDefault in onClick could be called.

Thanks @karelskopek and @Mintaffee for your help with digging into this.

@aweary Do you think this is worth solving?

No progress with this one? I'm using React v16.2 and the behaviour is the same - onChange is triggered first time and subsequent clicks doesn't trigger it. I would say quite high priority issue here..

Is this related? preventDefault called in a checkbox onChange causes React and the browser state to go out of sync: https://codesandbox.io/s/0qpr936xkv

Still an issue in React 16.8.6. One possible workaround is to do everything in onClick handler:

const radios = ['one', 'two', 'three'];

class Row extends Component {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  render() {
    const { value } = this.props;
    return (
      <div className="row">
        <div className="radio-group">
          {radios.map(radio => (
            <label key={radio} className="radio">
              <input
                type="radio"
                name="radio"
                value={radio}
                checked={radio === value}
                onClick={this.onClick}
                onChange={this.onChange}
              /><span>Radio</span>
            </label>
          ))}
        </div>
      </div>
    );
  }

  onClick(event) {
    // Check something to prevent radio change
    if (...) {
      event.preventDefault();
      return;
    }
    // Sync value in the store
    this.props.setValue(event.target.value);
  }
  onChange() {
    // We need this empty handler to suppress React warning:
    // "You provided a `checked` prop to a form field without an `onChange` handler.
    // This will render a read-only field. If the field should be mutable use `defaultChecked`.
    // Otherwise, set either `onChange` or `readOnly`"
  }
}

If you use controlled component another option I found is to simply leave value in the state unchanged:

onChange(event) {
  // Check something to prevent radio change
  if (...) {
    return;
  }
  // Sync value in the store
  this.props.setDateTime(event.target.value);
}

For a simpler solution, you can do e.preventDefault on onMouseDown event to prevent onChange from firing.

onMouseDown(e) { if (...) { e.preventDefault(); return; } }

Was this page helpful?
0 / 5 - 0 ratings