React: Event Handlers

Now that we have a lot of foundational React knowledge, it's time to dive into the complex topic of interactivity. This is where React shines - building user interfaces that react to changes in data, whether those changes are driven by API or user interactions. We'll focus on user interactions first, starting with basic click events.

Event Listeners

In vanilla JavaScript, events are a set of interaction APIs available in the browser that can be listened for, responded to, and used to trigger interactions or functionality. For example, when a user clicks on an "X" button, a prompt is dismissed; when a user submits a form, we show a loading spinner. In the following example, clicking on the "Click me!" button triggers a window alert that greets you:

This can happen in a couple of ways. You can listen for the click event using an event listener, passing a callback function that you want to call. You attach the event listener to the element using a query selector:

<button class="greeting">
  Click me!
</button>
const button = document.querySelector('.greeting');

button.addEventListener('click', function() {
  window.alert("Howdy!")
})

Or, you can include an onclick attribute on the button, which calls a function:

<button class="greeting" onclick="greet()">
  Click me!
</button>
function greet() {
  window.alert('Howdy!');
}

In React

React follows the second pattern, allowing us to put an event handler right in the JSX:

export default function Greeting() {
  function greet() {
    window.alert("Howdy!");
  }
  return (
    <button onClick={greet}>
      Click me!
    </button>
  );
}

You can simply define the function you want to call within the component that it gets called, and then attach it using onClick. Note the camelCase here as opposed to onclick in the previous HTML example - more on this in a second.

This pattern is largely the recommended way of doing things in React. We do sometimes have to use addEventListener() for window-level events, but we should try to use the on[X] props like onClick and onChange whenever possible. Here are some reasons why:

  1. Automatic cleanup. Whenever we add an event listener, we're also supposed to remove it when we're done with it with removeEventListener. Forgetting to do so can introduce a memory leak. React automatically removes listeners for us when we use on[X] prop functions.
  2. Improved performance. By giving React control over the event listeners, it can optimize things for us, like batching multiple event listeners together to reduce memory consumption.
  3. No DOM interaction. React likes for us to stay within the abstraction of the virtual DOM. We typically try to avoid interacting with the DOM directly. addEventListener() leans heavily on querySelector(), which is something we should avoid.

React Props vs. HTML Event Handlers

There are a couple of differences to note between passing the handler in React vs. in HTML, notably around using camelCase for the prop/attribute, passing a function reference as opposed to a function call, and using a wrapper function to allow us to call the handler with arguments.

camelCase the React Prop

In HTML the attribute is onclick, but in React it's onClick:

HTML:

<button class="greeting" onclick="greet()">
  Click me!
</button>

React:

export default function Greeting() {
  function greet() {
    window.alert("Howdy!");
  }
  return (
    <button onClick={greet}>
      Click me!
    </button>
  );
}

Same with onChange, onKeyDown, onTransitionEnd, etc. React does a good job with warnings for this one.

Passing a Function Reference vs. a Function Call

In HTML, you pass the function call to the handler:

<button class="greeting" onclick="greet()">
  Click me!
</button>

Note the parenthesis () at the end of onclick="greet()".

In React, you omit the () parenthesis, passing a function reference to the handler.

✅ We want to do this:

export default function Greeting() {
  function greet() {
    window.alert("Howdy!");
  }
  return (
    <button onClick={greet}>
      Click me!
    </button>
  );
}

🚫 We don't want to do this:

export default function Greeting() {
  function greet() {
    window.alert("Howdy!");
  }
  return (
    <button onClick={greet()}>
      Click me!
    </button>
  );
}

Including the () parenthesis would cause the function to call right away, instead of waiting for the handler to call it. Here is the same code transpiled to plain JavaScript:

✅ Correct. This will be called when the user clicks the button:

React.createElement(
  'button',
  {
    onClick: greet,
  },
  'Click me!',
)

🚫 Incorrect. This will be called immediately, when the element is created:

React.createElement(
  'button',
  {
    onClick: greet(),
  },
  'Click me!',
)

Specifying Arguments

Because we have to pass a function reference, if we want our handlers to accept arguments when the function gets called, we have to do a little bit of funny business.

Say, for example, we modify our `greet() function to accept a different word for the greeting alert:

function greet(message) {
  window.alert(`${message}!`);
}

// Alert "Howdy!":
greet('Howdy');

// Alert "Hello there!":
greet('Hello there');

Since we can't pass parenthesis directly to a prop handler function, how might we pass in arguments?

🚫 Incorrect. This calls the function without any argument:

export default function Greeting() {
  function greet(message) {
    window.alert(`${message}!`);
  }
  return (
    <>
      <button onClick={greet}>
        Click to say Howdy!
      </button>

      <button onClick={greet}>
        Click to say Hello there!
      </button>
    </>
  );
}

If we pass it the traditional way it calls it immediately:

🚫 Also Incorrect. This calls the function immediately:

export default function Greeting() {
  function greet(message) {
    window.alert(`${message}!`);
  }
  return (
    <>
      <button onClick={greet('Howdy')}>
        Click to say Howdy!
      </button>

      <button onClick={greet('Hello there')}>
        Click to say Hello there!
      </button>
    </>
  );
}

We can solve this problem using a wrapper function:

✅ Correct: This allows us to pass the argument using a wrapper function:

export default function Greeting() {
  function greet(message) {
    window.alert(`${message}!`);
  }
  return (
    <>
      <button onClick={() => greet("Howdy")}>
        Click to say Howdy!
      </button>

      <button onClick={() => greet("Hello there")}>
        Click to say Hello there!
      </button>
    </>
  );
}

This transpiles to:

React.createElement(
  'button',
  {
    onClick: () => greet('Howdy'),
  },
  'Click to say Howdy!',
);

React.createElement(
  'button',
  {
    onClick: () => greet('Hello there'),
  },
  'Click to say Hello there!',
);

Now when the button gets clicked, the anonymous arrow function gets called, which in turn calls greet('Howdy') or greet('Hello there').

.bind()

We could also use .bind() but there are no real clear advantages (unless you need to use this, which is very rare in React).

✅ Valid. But probably not as clear what's going on, and really only useful if you're using this:

export default function Greeting() {
  function greet(message) {
    window.alert(`${message}!`);
  }
  return (
    <>
      <button onClick={greet.bind(null, "Howdy")}>
        Click to say Howdy!
      </button>

      <button onClick={greet.bind(null, "Hello there")}>
        Click to say Hello there!
      </button>
    </>
  );
}

Review

We are starting to get into some of React's interactivity, which is what React specializes in. In this post I've covered some of the gotchas coming from HTML and vanilla JavaScript to React:

  • For an event handler, you can define the function you want to call within the component that it gets called in
  • You attach the event handler to the JSX element using on[X], where [X] is your desired event. Unlike the traditional HTML attribute, this prop name is camelCased. For example:
    • onClick instead of onclick
    • onChange instead of onchange
    • onKeyDown instead of onkeydown
    • onTransitionEnd instead of ontransitionend
  • In HTML, you pass a function call (i.e., onclick="greet()"), but in React you pass a function reference (i.e., onClick={greet}, omitting the () parenthesis)
    • If you want to pass arguments to your event handler, you can do so using a wrapper function: onClick={() => greet("Howdy")}
    • You can also use .bind(), but this typically doesn't offer any advantages: onClick={greet.bind(null, "Howdy")}