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…
Next week, on Forms…
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:
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.