React & event.preventDefault()

We recently shipped a UX improvement where we replaced our simplistic address fields with a Google Place Autocomplete Address Form.

Queue broadcast voice…

Previously, on Forms…

Address + Zip Code lookup.

Next week, on Forms…

Fewer keystrokes & power of Google.

If you’re anything like me, you probably rarely use your mouse and instead press Enter on your keyboard. Apparently so does everyone else.

Pressing Enter would immediately submit the form before the formatted address was converted into individual fields.

Luckily, I know how to JavaScript, so I’ll just prevent Enter from propagating up:

handleKeyDown(event) {
  if (event.keyCode === 13) {
    event.preventDefault(); // Let's stop this event.
    event.stopPropagation(); // Really this time.

    alert("Is it stopped?");

    // "Hahaha, I'm gonna submit anyway!" - Chrome
  }
}

Why Don’t Events in React Work as Expected?

After several choice words, a comment buried in Stack Overflow offers this:

React’s actual event listener is also at the root of the document, meaning the click event has already bubbled to the root. You can use event.nativeEvent.stopImmediatePropagation to prevent other event listeners from firing, but order of execution is not guaranteed. — Stack Overflow

Sure enough, React’s documentation Interactivity and Dynamic UIs inexplicably says:

React doesn’t actually attach event handlers to the nodes themselves. When React starts up, it starts listening for all events at the top level using a single event listener.

So, this is great for memory usage, but insanely confusing when you attempt to leverage the standard event APIs. In fact, React events are actually Synthetic Events, not Native Events.

With React, events are to be observed, not mutated or intercepted.

A Wild Solution Appears

Because our forms produce HTTP GET/POST-friendly HTML, we wanted to ensure that standard functionality was not broken (e.g. pressing Enter, or pressing Go on a mobile keyboard).

However, we still needed to ensure that normal user behavior didn’t break our improved functionality.

The solution was to go back to Native Events.

componentDidMount() {
  // Direct reference to autocomplete DOM node
  // (e.g. <input ref="autocomplete" ... />
  const node = React.findDOMNode(this.refs.autocomplete);

  // Evergreen event listener || IE8 event listener
  const addEvent = node.addEventListener || node.attachEvent;

  addEvent(“keypress”, this.handleKeyPress, false);
}

componentWillUnmount() {
  const removeEvent = node.removeEventListener || node.detachEvent;

  // Reduce any memory leaks
  removeEvent("keypress", this.handleKeyPress);
}

handleKeyPress(event) {
  // [Enter] should not submit the form when choosing an address.
  if (event.keyCode === 13) {
    event.preventDefault();
  }
}

Sure enough, if you’re using React v0.14 already, there’s a library for this:

https://github.com/erikras/react-native-listener


I’m genuinely surprised that this expectation of event handling is quietly broken in React.

I would have expected that, since events are turned into SyntheticEvents, the event.preventDefault() and event.stopPropagation() would call console.warn or console.error due to the break in functionality.

This is more of an edge-case due to our forms actually submitting when the user presses Enter, and not a situation others come across unwittingly.