Twilio, Github, and Under Armour gain complete visibility with Lightstep

See how!

Technical


Beyond onClick: Handling Events in React


Dan Hedgecock

by Dan Hedgecock

Explore More Technical Blogs

Dan Hedgecock

by Dan Hedgecock


01-22-2020

Looking for Something?

No results for 'undefined'

Great modern web applications are highly interactive and effective event management is critical to giving users the control they want. Luckily for us, React makes handling events in a cross-browser compatible way easy by abstracting the underlying DOM mechanisms into a consistent framework API. Thanks to React, you can pass a callback function to the onClick handler for any element and it will be called on click events with a standard SyntheticEvent object that has consistent properties across browsers. React SyntheticEvents are a big upgrade from the past when event handlers required browser-specific quirks to be addressed!

Events in React

The preferred way to listen for events in React is by passing event handlers to your JSX elements. Some use cases require directly adding event listeners to the DOM, but this isn’t recommended for a few reasons we’ll cover later.

Using React Event Handlers

Events setup with React event handlers will be an application’s primary way to listen for and manage events. The important differences between React event handlers and native DOM event handlers is the names are camelcase instead of lowercase, you pass in functions instead of strings, and you have to explicitly call stopPropagation instead of returning false. Besides that, you should be good to go. You can see the full list of supported events here

// A typical click event handler
function ButtonComponent() {
  return (
    <button
      type=”button”
      onClick={event => {
        window.alert("Confirmed!");
      }}
    >
      Confirm
    </button>
  );
}

If you want to handle the event in the capture phase instead of the bubbling phase, you just add ‘Capture’ to the end of the event handler name:

<button onClickCapture={(e) => console.log(‘Fires before bubbling’, e)} />

Functions for handling events are often defined in parents and passed to children before being set as an event handler prop. In these situations, if you’re working with components that need render optimizations, remember that functions defined in parent components may be redefined on each render, and you’ll need to use the useCallback hook to enable child props comparisons. Here’s how you can do that:

function ParentComponent() {
  // Create a memoized function that can be compared across renders for child
  // components that need to be optimized
  const memoizedEventHandler = useCallback(() => {
    doSomethingAwesome();
  }, []);

  return <ChildComponent clickHandler={memoizedEventHandler} />;
}

Using a DOM addEventListener

Sometimes you need to listen for an event that occurs outside of a component’s scope. For example, you might want to have a component perform some action when a user types a specific keybinding, or you might want to rerender a data visualization when the browser dimensions change. In these cases, you’ll need to use the DOM addEventListener API directly.

In these cases, you need to manage the lifecycle of the listener, and be aware of how mixing React event handlers with native DOM event listeners can create timing issues, specifically:

  • You’ll need to manage removing the event listener at the appropriate time to prevent memory leaks.
  • You should know that native event listeners are always called before React’s event handlers, so if you call stopPropogation on a native event it could prevent every React event handler listening for that event (including parent listeners) from ever seeing the event.

The useEffect hook (or the componentDidMount and componentWillUnmount lifecycle methods in class components) is the best way to safely use a DOM event listener, eg:

// The useEffect hook lets us add an event listener when the component is mounted
// and then clean it up when unmounted
useEffect(() => {
  // Create a function that will be called by our event listener that we can
  // also pass to removeEventListener, this one will perform some component action
  // when a user types cmd+enter (or ctrl+enter on Windows)
  function activateFeatureListener(event) {
    const { keyCode, ctrlKey, metaKey } = event;
    if (keyCode === 13 && (ctrlKey || metaKey)) {
      handleActivateFeature();
    }
  }

  // Setup the actual listener for the keydown event on the document
  document.addEventListener('keydown', activateFeatureListener);

  // Be sure to remove the listener when the component is unmounted to prevent
  // memory leaks!
  return function cleanup() {
    document.removeEventListener('keydown', activateFeatureListener);
  };
}, []);

You probably want to avoid this method as much as possible and use React's synthetic events instead. That being said, hooks are very useful for functional components, and the docs help give you a good idea of how they work and what they can be used for.

Events in the Browser

History of Events

There are two opposite models stages of event handling, capturing and bubbling.

The bubbling model, developed by Microsoft has the event first hit the innermost dom element and then hit its parent, and its parents parent, all the way up to the window object.

The capturing model was developed by Netscape and is the opposite. The event will trigger handlers on the root most element on the Dom and trickle down to the innermost child affected.

You can remember the two with the phrase "trickle down, bubble up".

The W3C model currently adopted is to capture the event at the parent most element and let it trickle down to the innermost child element affected, and then to bubble back up that same chain.

Categories of Events

You'll find a complete reference at the excellent event docs provided by Mozilla. There's a wide variety of categories including (but not limited to): CSS animations, web socket events, printing, mouse, keyboard, and focus. Certainly the most commonly used are mouse, keyboard, and focus but it's neat to explore and think up different ways you could make a more immersive experience for your users.

Order of Events

Order matters. There's no definitive guide I found out there detailing the priorities of which events are fired and when. Sometimes it takes the reference and some fiddling to figure out what will fire first between two events. For instance, clicking out of an input will fire a 'mousedown' event, then a focus 'blur' event, then a 'mouseup event', then a 'click event'.

And of course, you can try to capture the event on the way down the chain to preempt event handlers triggered by the event bubbling back up.

You can see this in action by playing with the following example in your browser with the console open.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Event handling demo</title>
  </head>
  <body>
    <section id="section">
      <nav id="nav">
        <ul id="ul">
          <li id="nav-one">Home</li>
          <li id="nav-two">About</li>
        </ul>
      </nav>
      <main id="main">
        <span id="span">My text is awesome</span>
        <input id="input" type="text">
      </main>
    </section>
    <script type="text/javascript">
      function logEvent(e, elem, event, capture) {
        console.log(elem + " registered an " + event + " event " + " with capture set to " + capture + ". The event: " + JSON.stringify(e));
      }

      document.addEventListener("mousedown", (e) => logEvent(e, "document", "mousedown", true), true);
      document.addEventListener("mousedown", (e) => logEvent(e, "document", "mousedown", false), false);
      window.addEventListener("mousedown", (e) => logEvent(e, "window", "mousedown", true), true);
      window.addEventListener("mousedown", (e) => logEvent(e, "window", "mousedown", false), false);
      document.getElementById("section").addEventListener("mousedown", (e) => logEvent(e, "section", "mousedown", true), true);
      document.getElementById("section").addEventListener("mousedown", (e) => logEvent(e, "section", "mousedown", false), false);

      document.addEventListener("click", (e) => logEvent(e, "document", "click", true), true);
      document.addEventListener("click", (e) => logEvent(e, "document", "click", false), false);
      window.addEventListener("click", (e) => logEvent(e, "window", "click", true), true);
      window.addEventListener("click", (e) => logEvent(e, "window", "click", false), false);
      document.getElementById("section").addEventListener("click", (e) => logEvent(e, "section", "click", true), true);
      document.getElementById("section").addEventListener("click", (e) => logEvent(e, "section", "click", false), false);

      document.addEventListener("mouseup", (e) => logEvent(e, "document", "mouseup", true), true);
      document.addEventListener("mouseup", (e) => logEvent(e, "document", "mouseup", false), false);
      window.addEventListener("mouseup", (e) => logEvent(e, "window", "mouseup", true), true);
      window.addEventListener("mouseup", (e) => logEvent(e, "window", "mouseup", false), false);
      document.getElementById("section").addEventListener("mouseup", (e) => logEvent(e, "section", "mouseup", true), true);
      document.getElementById("section").addEventListener("mouseup", (e) => logEvent(e, "section", "mouseup", false), false);

    document.getElementById("input").addEventListener("blur", (e) => logEvent(e, "input", "blur", true), true);
    document.getElementById("input").addEventListener("blur", (e) => logEvent(e, "input", "blur", false), false);
    </script>
  </body>
</html>

It might surprise you that mouseup fires before click. It surprised me! The third boolean argument in `addEventListenter` sets the capture flag which indicates whether you want this event handler to fire on the capture or bubble phase of the event's propagation.

Default

Events have a read-only field `cancelable`. If that field is true then you may call the `preventDefault` method on the event (you can call it if the field is false it just has no effect).

You can cancel a wide variety of actions including scrolls, clicks, and checkbox toggling.

Event propagation continues as normal, but canceling at any layer of the event chain will cancel the event for every stage.

Propagation

As per the W3C model, events propagate first down from the root to the innermost child element and then bubble back up. You can prevent this at any level of the chain with the `stopPropagation` method. Calling `stopPropagation` will prevent the next element in the event chain and all others past it from handling the event.

If you need to also prevent other event handlers on the same element from firing you may call `stopImmediatePropagation`. If you had multiple click handlers on the same `div` element, for example, this would prevent all the handlers after the handler with the `stopImmediatePropagation` from firing.

Note that stopping the propagation of a mousedown event will not prevent the mouseup event from firing. The example below can demonstrate how propagations are stopped.

<!DOCTYPE html>
<html>
  <head>
  <meta charset="utf-8" />
  <title>Event handling demo</title>
  </head>
  <body>
  <section id="section">
    <nav id="nav">
      <ul id="ul">
        <li id="nav-one">Home</li>
        <li id="nav-two">About</li>
      </ul>
    </nav>
    <main id="main">
      <span id="span">My text is awesome</span>
      <input id="input" type="text">
    </main>
  </section>
  <script type="text/javascript">
    function logEvent(e, elem, event, capture) {
      console.log(elem + " registered an " + event + " event " + " with capture set to " + capture + ". The event: " + JSON.stringify(e));
    }

    function stopProp(e, elem, event, capture) {
      console.log(elem + " registered an " + event + " event " + " with capture set to " + capture + ". The event: " + JSON.stringify(e) + " - STOPPING PROPAGATION");
      e.stopPropagation();
    }

    function stopImmProp(e, elem, event, capture) {
      console.log(elem + " registered an " + event + " event " + " with capture set to " + capture + ". The event: " + JSON.stringify(e) + " - STOPPING PROPAGATION IMMEDIATELY");
      e.stopImmediatePropagation();
    }

    document.getElementById("nav-one").addEventListener("mousedown", (e) => logEvent(e, "nav-one", "mousedown", true), true);
    document.getElementById("nav-one").addEventListener("mousedown", (e) => logEvent(e, "nav-one", "mousedown", false), false);
    document.getElementById("nav-one").addEventListener("mouseup", (e) => logEvent(e, "nav-one", "mouseup", true), true);
    document.getElementById("nav-one").addEventListener("mouseup", (e) => logEvent(e, "nav-one", "mouseup", true), true);

    document.getElementById("nav-two").addEventListener("click", (e) => logEvent(e, "nav-two", "click", true), true);
    document.getElementById("nav-two").addEventListener("click", (e) => stopImmProp(e, "nav-two", "click", true), true);
    document.getElementById("nav-two").addEventListener("click", (e) => logEvent(e, "nav-two-after-stop-imm", "click", true), true);
    document.getElementById("nav-two").addEventListener("click", (e) => logEvent(e, "nav-two-after-stop-imm", "click", false), false);
    document.getElementById("nav-two").addEventListener("mouseup", (e) => logEvent(e, "nav-two", "mouseup", true), true);

    document.getElementById("ul").addEventListener("mousedown", (e) => stopProp(e, "ul", "mousedown", true), true);
    document.getElementById("nav").addEventListener("mousedown", (e) => logEvent(e, "nav", "mousedown", true), true);
    document.getElementById("nav").addEventListener("mouseup", (e) => logEvent(e, "nav", "mouseup", true), true);
  </script>
  </body>
</html>

Hopefully, this has been a good primer on how events work, where the model came from, and how best to use them in React applications. For more information, you can check out this article on event order.

 

Explore More Technical Blogs