-- online Search!

Events in React

by kirupa   |   filed under Learning React

So far, all of our examples only did their work on page load. As you probably guessed, that isn't normal. In most apps, especially the kind of UI-heavy ones we will be building, there is going to be a ton of things the app does only as a reaction to something. That something could be triggered by a mouse click, a key press, window resize, or a whole bunch of other gestures and interactions. The glue that makes all of this possible is something known as events.

Now, you probably know all about events from your experience using them in the DOM world. (If you don't, then I suggest getting a quick refresher first.) The way React deals with events is a bit different, and these differences can surprise you in various ways if you aren't paying close attention. Don't worry. That's why you have this tutorial. We will start off with a few simple examples and then gradually look at increasingly more bizarre, complex, and (yes!) boring things.

Onwards!

OMG! A React Book Written by Kirupa?!!

To kick your React skills up a few notches, everything you see here and more (with all its casual clarity!) is available in both paperback and digital editions.

BUY ON AMAZON

Listening and Reacting to Events

The easiest way to learn about events in React is to actually use them, and that's exactly what we are going to do! To help with this, we have a simple example made up of a counter that increments each time you click on a button. Initially, our example will look like this:

A yellow rounded box with a large 0 in the middle and a small plus button below it.

Each time you click on the plus button, the counter value will increase by 1. After clicking the plus button a bunch of times, it will look sorta like this:

The same yellow box, now showing the number 13.

Under the covers, the way this example works is pretty simple. Each time you click on the button, an event gets fired. We listen for this event and do all sorts of React-ey things to get the counter to update when this event gets overheard.

Starting Point

To save all of us some time, we aren't going to be creating everything in our example from scratch. By now, you probably have a good idea of how to work with components and styles. Instead, we are going to start off with a partially implemented example that contains everything except the event-related functionality that we are here to learn.

In a Vite project (if you don't have one, here is how to set one up), empty out the src/index.css file and replace the contents of src/App.jsx with the following:

import { useState } from "react";

function Counter({ display }) {
  const textStyle = {
    fontSize: 72,
    fontFamily: "sans-serif",
    color: "#333",
    fontWeight: "bold",
  };

  return <div style={textStyle}>{display}</div>;
}

function CounterParent() {
  const [count, setCount] = useState(0);

  const backgroundStyle = {
    padding: 50,
    backgroundColor: "#FFC53A",
    width: 250,
    height: 100,
    borderRadius: 10,
    textAlign: "center",
  };

  const buttonStyle = {
    fontSize: "1em",
    width: 30,
    height: 30,
    fontFamily: "sans-serif",
    color: "#333",
    fontWeight: "bold",
    lineHeight: "3px",
  };

  return (
    <div style={backgroundStyle}>
      <Counter display={count} />
      <button style={buttonStyle}>+</button>
    </div>
  );
}

function App() {
  return <CounterParent />;
}

export default App;

Once you have added all of this, preview everything in your browser to make sure things work. You should see the beginning of our counter. Take a few moments to look at what all of this code does. There shouldn't be anything that looks strange, except maybe the highlighted line. The useState call gives us a count value that starts at 0, and a setCount function for changing it. Whenever we call setCount with a new value, React updates the value and re-renders our component so the screen catches up. We'll spend a lot more time on this in the Meet React Hooks and Dealing with State tutorials. For now, just treat it as a counter that React knows how to update.

The only odd thing about our example is that clicking the plus button won't do anything. We'll fix that right up in the next section.

Making the Button Click Do Something

Each time we click on the plus button, we want the value of our counter to increase by one. What we need to do is going to roughly look like this:

  1. Listen for the click event on the button.
  2. Implement the event handler where we react to the click and increase the value of our count state that our counter relies on.

We'll just go straight down the list...starting with listening for the click event. In React, you listen to an event by specifying everything inline in your JSX itself. More specifically, you specify both the event you are listening for and the event handler that will get called all inside your markup. To do this, find the return statement inside our CounterParent component, and make the following highlighted change:

return (
  <div style={backgroundStyle}>
    <Counter display={count} />
    <button onClick={increase} style={buttonStyle}>+</button>
  </div>
);

What we've done is told React to call the increase function when the onClick event is overheard. Notice that event names in React are camelCase (onClick, not onclick), and that we pass in the function itself in curly braces. Next, let's go ahead and implement the increase function, aka our event handler. Inside our CounterParent component, add the following highlighted lines right below the useState line:

function CounterParent() {
  const [count, setCount] = useState(0);

  function increase(e) {
    setCount(count + 1);
  }

  // backgroundStyle, buttonStyle, and the return statement follow
}

All we are doing with these lines is making sure that each call to the increase function increments the value of our count by 1. Because we are dealing with events, our increase function (as the designated event handler) will get access to the event argument. We have set it to be accessed by e, and you can see that by looking at our increase function's signature (aka what its declaration looks like). We'll talk about the various events and their properties in a little bit.

Now, go ahead and preview what you have in your browser. Once everything has loaded, click on the plus button to see all of our newly added code in action. Our counter value should increase with each click! Isn't that pretty awesome?

Pass the Function, Don't Call It

It is really easy to write onClick={increase()} with a set of parentheses at the end. Don't! With the parentheses, we are calling increase while React is rendering our component instead of when somebody clicks. That ends up changing our state in the middle of a render, which triggers another render, which calls increase again, and so on. React will stop this party with a Too many re-renders error. What we want is onClick={increase}, which hands React the function so that it can call it later when the click happens.

Event Properties

As you know, our events pass what are known as event arguments to our event handler. These event arguments contain a bunch of properties that are specific to the type of event you are dealing with. In the regular DOM world, each event has its own type. For example, if you are dealing with a mouse event, your event and its event arguments object will be of type MouseEvent. This MouseEvent object will allow you to access mouse-specific information like which button was pressed or the screen position of the mouse click. Event arguments for a keyboard-related event are of type KeyboardEvent. Your KeyboardEvent object contains properties which (among many other things) allow you to figure out which key was actually pressed. I could go on forever for every other Event type, but you get the point. Each Event type contains its own set of properties that you can access via the event handler for that event!

Why am I boring you with things you already know? Well...

Meet Synthetic Events

In React, when you specify an event in JSX like we did with onClick, you are not directly dealing with regular DOM events. Instead, you are dealing with a React-specific event type known as a SyntheticEvent. Your event handlers don't get native event arguments of type MouseEvent, KeyboardEvent, etc. They always get event arguments of type SyntheticEvent that wrap your browser's native event instead. What is the fallout of this in our code? Surprisingly not a whole lot.

Each SyntheticEvent contains the following properties (and a few more):

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
DOMEventTarget target
number timeStamp
string type

These properties should seem pretty straightforward...and generic! The non-generic stuff depends on what type of native event our SyntheticEvent is wrapping. This means that a SyntheticEvent that wraps a MouseEvent will have access to mouse-specific properties such as the following:

boolean altKey
number button
number buttons
number clientX
number clientY
boolean ctrlKey
boolean getModifierState(key)
boolean metaKey
number pageX
number pageY
DOMEventTarget relatedTarget
number screenX
number screenY
boolean shiftKey

Similarly, a SyntheticEvent that wraps a KeyboardEvent will have access to these additional keyboard-related properties:

boolean altKey
string code
boolean ctrlKey
boolean getModifierState(key)
string key
string locale
number location
boolean metaKey
boolean repeat
boolean shiftKey

In the end, all of this means that you still get the same functionality in the SyntheticEvent world that you had in the vanilla DOM world. If you ever need something that React's wrapper doesn't expose, the original browser event is available via the nativeEvent property.

Now, here is something I learned the hard way. Don't refer to traditional DOM event documentation when using Synthetic events and their properties. Because the SyntheticEvent wraps your native DOM event, events and their properties may not map one-to-one. Some DOM events don't even work the same way in React. To avoid running into any issues, if you want to know the name of a Synthetic event or any of its properties, refer to the React documentation instead.

No More Event Pooling

If you read older React tutorials, you may come across a warning about holding on to the event object after your handler finishes. Back in React 16 and earlier, React reused (pooled) event objects for performance, so reading e.target inside a setTimeout or after an await would give you null unless you called e.persist() first. That pooling went away in React 17. You can use the event whenever you like, and you don't need e.persist() anymore. (If you see it in older code, it is now harmless and does nothing.)

Doing Stuff With Event Properties

By now, you've probably seen more about the DOM and Synthetic events than you'd probably like. To wash away the taste of all that text, let's write some code and put all of this new found knowledge to good use. Right now, our counter example increments by one each time you click on the plus button. What we want to do is increment our counter by ten when the Shift key on the keyboard is pressed while clicking the plus button with our mouse.

The way we are going to do that is by using the shiftKey property that exists on the SyntheticEvent when using the mouse. The way this property works is simple. If the Shift key is pressed when this mouse event fires, then the shiftKey property value is true. Otherwise, the shiftKey property value is false. To increment our counter by 10 when the Shift key is pressed, go back to our increase function and make the following highlighted changes:

function increase(e) {
  let currentCount = count;
  if (e.shiftKey) {
    currentCount += 10;
  } else {
    currentCount += 1;
  }
  setCount(currentCount);
}

Once you've made the changes, preview our example in the browser. Each time you click on the plus button, your counter will increment by one just like it had always done. If you click on the plus button with your Shift key pressed, notice that our counter increments by 10 instead.

The reason that all of this works is because we change our incrementing behavior depending on whether the Shift key is pressed or not. If the shiftKey property on our SyntheticEvent event argument is true, we increment our counter by 10. If the shiftKey value is false, we just increment by 1.

More Eventing Shenanigans

We are not done yet! Up until this point, we've looked at how to work with events in React in a very simplistic way. In the real world, rarely will things be as direct as what we've seen. Your real apps will be more complex, and because React insists on doing things a bit differently, we'll need to learn (or re-learn) some new event-related tricks and techniques to make our apps work. That's where this section comes in. We are going to look at some common situations you'll run into and how to deal with them.

Passing Arguments to an Event Handler

Our increase function only cares about the event. Sometimes your handler needs more than that, such as which item in a list was clicked. The trick is to wrap your handler in an arrow function and call it with whatever you need:

{colors.map((c) => (
  <button key={c} onClick={(e) => pick(c, e)}>
    {c}
  </button>
))}

Here, the arrow function is the event handler that React calls on click. Inside it, we call our own pick function with the c for that particular button and the event itself. Notice that the call to pick has parentheses here, and that is fine. They are inside the arrow function, so they don't run until the click happens.

Components and Events

Let's say your component is nothing more than a button or another type of UI element that users will be interacting with. Here is a PlusButton component and a use of it that looks totally reasonable:

function PlusButton() {
  return <button>+</button>;
}

function CounterParent() {
  // ...
  return <PlusButton onClick={increase} />;
}

When somebody clicks on our PlusButton, nothing happens. The reason is that React only wires up events on built-in elements such as button and div. On a component, onClick is nothing special. It is a regular prop that just happens to have a name that looks like an event. Our PlusButton receives it and does absolutely nothing with it.

The fix is to take that prop and hand it to the element that should be listening. We can do that by naming the prop whatever we like and passing it on to the button:

function PlusButton({ clickHandler }) {
  return <button onClick={clickHandler}>+</button>;
}

function CounterParent() {
  // ...
  return (
    <div>
      <PlusButton clickHandler={increase} />
    </div>
  );
}

On our button element, we specify the onClick event and set its value to the clickHandler prop. At runtime, this prop gets evaluated as our increase function, and clicking the plus button ensures the increase function gets called. This solves our problem while still allowing our component to participate in all this eventing goodness!

If you want the component to behave like a regular button that accepts any event, you can use the spread trick from the Transferring Properties tutorial:

function PlusButton(props) {
  return <button {...props}>+</button>;
}

Now <PlusButton onClick={increase} /> works just like we first expected, and so would onMouseEnter, onKeyDown, or any other prop you pass in.

A quick note on naming. The convention in React is to name props that take a function on followed by the thing that happens (onPlus, onDelete), and to name the functions that respond to them handle followed by the same thing (handlePlus, handleDelete). You'll then end up with code like <PlusButton onPlus={handlePlus} />, which reads nicely.

Listening to Regular DOM Events

If you thought the previous section was a doozy, wait till you see what we have here. Not every event that you can listen for is available as an inline prop on an element. For example, the resize event fires on the window itself, and you don't have a JSX element for the window to put an onResize on. For events like these, you use the traditional addEventListener approach with a few extra hoops to jump through.

Take a look at the following component:

import { useEffect, useState } from "react";

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return <p>The window is {width}px wide.</p>;
}

We have our WindowWidth component that listens for the resize event and displays the current width of the window. We start listening by calling addEventListener and specifying both the event and the event handler to call. We do all of this inside something called useEffect, which is a place for code that reaches outside of React and needs to run after our component appears on the screen.

That should be pretty straightforward. The only other thing you need to keep in mind is removing the event listener when the component goes away. The function we return from useEffect is our cleanup, and it runs when our component is removed. Inside it, we call removeEventListener to ensure no trace of our event listening is left behind. Resize one of your browser windows while this example is running, and you'll see the number update. We'll look at useEffect in much more detail in the Effects with useEffect tutorial.

Stopping Defaults and Bubbling

Two of the properties we saw earlier on SyntheticEvent are used a lot: preventDefault() and stopPropagation(). They do the same things you are used to in the DOM world. Calling preventDefault() stops the browser's default action for the event, such as a link navigating to its address:

<a href="https://www.kirupa.com" onClick={(e) => e.preventDefault()}>
  This link goes nowhere
</a>

Calling stopPropagation() stops the event from bubbling up to the parent elements. In the following example, clicking the button does not trigger the div's handler:

<div onClick={() => console.log("div")}>
  <button onClick={(e) => {
    e.stopPropagation();
    console.log("button");
  }}>
    Click me
  </button>
</div>

Just like the DOM, events in React go through a capture phase on the way down to your element before bubbling back up. If you need to listen during the capture phase, add Capture to the end of the event name:

<div
  onClickCapture={() => console.log("1: div, capture")}
  onClick={() => console.log("3: div, bubble")}
>
  <button onClick={() => console.log("2: button")}>Click me</button>
</div>

Click the button, and you'll see the messages in the order of their numbers. The onClickCapture handler fires first, then the button's handler, and then the div's handler as the event bubbles back up.

What About this?

If you read older React code that uses classes, you'll see handlers wired up with this.increase = this.increase.bind(this) inside a constructor. That was needed because a class method loses its this when it is passed off as an event handler. With function components, there is no this to worry about, so you can forget about bind entirely. (If you want to be able to read those older components, the Function Components tutorial has you covered.)

React...why? Why?!

Before we call it a day, let's use this time to talk about why React decided to deviate from how we've worked with events in the past. There are two reasons:

  1. Consistency across browsers
  2. Improved performance

Let's elaborate on these two reasons a little bit.

Consistency Across Browsers

Event handling is one of those things that works mostly consistently in modern browsers, but small differences still exist in how events and their properties behave. By wrapping all of the native events as an object of type SyntheticEvent, React gives you one predictable interface and frees you from dealing with event handling quirks that you would otherwise have to deal with yourself.

Improved Performance

In complex UIs, the more event handlers you have, the more memory your app takes up. Manually dealing with that isn't difficult, but it is a bit tedious as you try to group events under a common parent. Sometimes, that just isn't possible. Sometimes, the hassle doesn't outweigh the benefits. What React does is pretty clever.

React doesn't attach your event handlers to each DOM element. Instead, it adds a single listener for each event type at the root container of your app (the element you passed to createRoot, which is usually the div with an id of root). When something is clicked, the browser's event bubbles up to the root, and that one listener figures out which of your handlers need to run and calls them:

A cyan box labeled root containing a green div box that contains a yellow button box. A pink arrow shows a click on the button bubbling up to the root, where React has a single click listener, and React then runs the matching handlers.

This technique is called event delegation. In React 16 and earlier, that single listener lived on the document. Starting with React 17, it lives on your root container instead. That means that if your page has more than one React app on it, each one handles its own events, and they don't step on each other.

This frees you from having to deal with optimizing your event handler-related code yourself. If you've manually had to do that in the past, you can relax knowing that React takes care of that tedious task for you. If you've never had to optimize event handler-related code yourself, consider yourself lucky :P

Conclusion

You'll spend a lot of time dealing with events, and this tutorial threw a lot of things at you. We started by learning the basics of how to listen to events and specify the event handler. We then looked at how React wraps events in its own SyntheticEvent type, and how components need to pass handlers on as props. Towards the end, we were all the way in the deep end and looking at eventing corner cases that you will bump into if you aren't careful enough. You don't want to bump into corners. That is never fun.

That useState and useEffect you saw along the way are examples of something React calls hooks. They are what makes function components able to remember things and react to the outside world, and they are what we'll look at next.

Next tutorial: Meet React Hooks

Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.

Your support keeps this site going! 😇

Kirupa's signature!

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2026 //--