DEV Community

loading...

I put 18 event handlers on a React element

ma5ly profile image Thomas Pikauli ・7 min read

Synthetic Event Party!

If you've ever built a React app you've probably encountered a SyntheticEvent. Good chance it was onChange, but maybe you've been a little more adventurous and it was onKeyDown.

In a technical sense a syntheticEvent is a wrapper that's part of React. It takes the place of the native event handlers you might know from plain Javascript.

Let's say we have a <button /> and we want something to happen when a user clicks on it. In plain Javascript we would add onclick to the element. That doesn't work in React. Instead the library provides its own handlers which mimic the functionality and make them work equally across browsers. They look a lot like the native handlers though. For example: onclick in React is onClick.

You can always read more about them in the docs.

Fire 'm up!

Now we could go through the entire list of events and explain them one-by-one, but to really get a sense of what's going on when you add one of these handlers to an element, let's just hook them up.

I've picked 18 of them. There are more, but these are the most common ones. We're going to add them to an <input /> element.

Since the objective is to get a feel for them, let's try to answer two questions:

  • when do they fire?
  • how often do they fire?

The first question we're going to answer by giving a visual cue upon firing, and the second question can be answered by keeping a log. Let's start building.

A synthetic event handler accepts a function. So we're going to add a function to all 18 handlers.

<input
  onCopy={() => this.addEvent("onCopy")}
  onCut={() => this.addEvent("onCut")}
  onPaste={() => this.addEvent("onPaste")}
  onKeyDown={() => this.addEvent("onKeyDown")}
  onKeyPress={() => this.addEvent("onKeyPress")}
  onKeyUp={() => this.addEvent("onKeyUp")}
  onFocus={() => this.addEvent("onFocus")}
  onBlur={() => this.addEvent("onBlur")}
  onChange={() => this.addEvent("onChange")}
  onClick={() => this.addEvent("onClick")}
  onDoubleClick={() => this.addEvent("onDoubleClick")}
  onMouseDown={() => this.addEvent("onMouseDown")}
  onMouseEnter={() => this.addEvent("onMouseEnter")}
  onMouseLeave={() => this.addEvent("onMouseLeave")}
  onMouseMove={() => this.addEvent("onMouseMove")}
  onMouseOver={() => this.addEvent("onMouseOver")}
  onMouseUp={() => this.addEvent("onMouseUp")}
  onSelect={() => this.addEvent("onSelect")}
/>
Enter fullscreen mode Exit fullscreen mode

As you might notice there is an anonymous in-line function that actually calls the real this.addEvent function. We have to do this because we want to pass an argument into the function; the name of the event.

The next step is to write the actual addEvent function. Before we write it, let's remember what we need to do. We need a visual cue upon each triggering of an event and we need to keep a count of each event being triggered. Let's actually start with the latter to see how many events fire. That could affect our idea of what we want to happen with regards to the visual cues.

Keeping a log

Our log of counts is a piece of data that changes upon user input. That means we're going to use state. The specific data structure we'll use is an array with objects inside of them. Each object will represent each type of synthetic event, and will have both a name property and an amount property. It would look like this:

[{ name: "onChange", amount: 1 }, { name: "onClick", amount: 5 }]
Enter fullscreen mode Exit fullscreen mode

Since we're starting out with an empty array without any counts, the first thing we need to do on each firing of the function is to check whether we need to add a new event to the array. If, however, we find that the event was already added to the array, we only need to increase the count.

addEvent = event => {
  const existingEvent = this.state.counts.filter(c => c.name === event)[0];
  const amount = existingEvent ? existingEvent.amount + 1 : 1;
  const count = this.state.counts.map(c => c.name).includes(event)
    ? Object.assign({}, existingEvent, { amount })
    : { name: event, amount };
};
Enter fullscreen mode Exit fullscreen mode

So the existingEvent will either contain data or remain empty. With that info we can determine the amount property. And finally we either have to update the existing object, or prepare a new one.

With that in place we need to update the state. Since our counts data is an array, and we now have an object, we need to either find and replace an existing object, or just tag the new object onto the array.

  const counts = produce(this.state.counts, draftState => {
    if (existingEvent) {
      const index = this.state.counts.findIndex(c => c.name === event);
      draftState[index] = count;
    } else {
      draftState.push(count);
    }
  });

  this.setState({counts})
Enter fullscreen mode Exit fullscreen mode

Now you might see an unfamiliar function here: produce. This is not a function I wrote myself, but one I exported from a library called immer. I highly recommend you check out that library if you are in the business of mutating data, but love your immutable data structures. immer allows you to work with your data as if you were directly mutating it, but via a 'draft state' keeps both your old and new state separated.

With that in place we now have a new version of our counts state we can put in the place of the current version of our counts state. The only thing that's left to do is to render this data onto the page, so we can actually see the counts.

In our render() function we can map our counts array into a list.

const counts = this.state.counts.map(c => {
  return (
    <li key={c.name}>
       {c.name} <strong>{c.amount}</strong>
    </li>
  );
});
Enter fullscreen mode Exit fullscreen mode

And in our return we can add the items to our <ul />.

 <ul>{counts}</ul>
Enter fullscreen mode Exit fullscreen mode

Now we should be able to see our synthetic events pop up with their respective counts. Try and see if you can fire up all 18 of them.

You might notice that events like onMouseMove fire up way more than others. This informs us that for our visual cues we have to be a bit mindful about that. And speaking about visual cues, let's set them up.

Party time

My idea is to render the name of the event on a random position on the screen on each trigger, and make it disappear again after a second or two. To make it a bit more clear which events fires, we will add specific styling for each event. Let's do that part first.

function getStyle(event) {
  let style;
  switch (event) {
    case "onCopy":
      style = {
        fontFamily: "Times New Roman",
        fontSize: 50,
        color: "red"
      };
      break;
    case "onCut":
      style = {
        fontFamily: "Tahoma",
        fontSize: 40,
        color: "blue"
      };
      break;
    case "onPaste":
      style = {
        fontFamily: "Arial",
        fontSize: 45,
        color: "salmon"
      };
      break;
  }
  return style;
}
Enter fullscreen mode Exit fullscreen mode

For reasons of brevity, these are not all 18 cases. You can find those in the full code, but you'll get the gist of it. Based on the event, we return a style object with a unique font size, font family and color.

The next part is to get the random position on the screen.

function getRandomNumber(min, max) {
  return Math.random() * (max - min) + min;
}

function getPosition() {
  return {
    left: getRandomNumber(0, window.innerWidth - 120),
    top: getRandomNumber(0, window.innerHeight - 120)
  };
}
Enter fullscreen mode Exit fullscreen mode

The getPosition function returns a style object with a random number between 0 and the width or height of the screen. I've deducted 120 pixels, so the events don't fall of the screen.

With these helpers in place, let's think about how to actually make the events show up on our screen. We have already implemented the counts so we have a bit of an idea how to do this. The difference is that this time we want to save each event as a separate object we can render on the screen, only to get rid of that object after 2 seconds of time. That means for each event we need to update the state twice.

Let's start with updating the state just once.

  const id = shortId.generate();
  const position = getPosition();
  const style = getStyle(event);
  const events = [...this.state.events, { id, event, position, style }];
Enter fullscreen mode Exit fullscreen mode

We first generate a unique id for each event using the shortid library. The reason for this, is that we need to be able to find the event again after it has been added to the state, so we can remove it.

Next we get our position and style object, which we'll need later to render the events on the screen. Finally, we create a new version of our events state.

If we update our state now and keep triggering events, we're going to get a huge array full of events, which will clog up the screen. So, we need to constantly clean up the array. How to do that?

An effective trick is to use setTimeOut, which is a little timer. After each update we wait 2 seconds, grab the id of the event we just added, and remove it again.

 this.setState({ events }, () => {
   setTimeout(() => {
     const events = this.state.events.filter(e => e.id !== id);
     this.setState({ events });
   }, 2000);
 });
Enter fullscreen mode Exit fullscreen mode

We start out with our regular setState in which we update the events array we just created. But then as a second argument we add a new anonymous function. By doing this in the second argument of setState we ensure the initial update of events has been applied.

Within that callback function, we set our timeout to 2 seconds, and create an updated version of our events with the now updated state. Since we are still in the same addEvent function, we know the id and we can easily filter it out. Then we set our state for the second time.

Now if we were to log this.state.events we should see it fill up and empty out. But it's more fun to see that on our screen. After all, we have a style object with random positions and unique formatting. So let's do a .map again in our component and see how it turned out.

const events = this.state.events.map(event => {
  return (
    <div
      key={event.id}
      style={{
        position: "absolute",
        left: event.position.left,
        top: event.position.top,
        zIndex: -1,
        opacity: 0.5,
        ...event.style
        }}
     >
      {event.event}
     </div>
   );
});
Enter fullscreen mode Exit fullscreen mode

As you can see we both add the position and styling of each event object to the element. We now only have to add the events variable to our return.

And with that we now have a nice synthetic event party on our screen. Aside from the visual fun we have just created, I hope you also get a feel for when each event triggers. Not each event will be super relevant in your day-to-day work, but sometimes it can be useful to know when onMouseLeave fires or just be aware that onDoubleClick exists.

See the full code in action here. Happy to chat on Twitter

Discussion (10)

pic
Editor guide
Collapse
dance2die profile image
Sung M. Kim • Edited

Thanks Thomas for the post~ 👍

The result is amazing (and pretty 🙂).
I never knew so many events can be fired and be handled.

This post and the CodeSandbox demo gave me a pretty good idea what event to throttle/debounce.


And you might also want to give @codesandbox a tweet with the Sandbox link to be picked on CodeSandbox Explore page (Refer to doc on how to get picked -> codesandbox.io/docs/explore#how-ca...)

Collapse
ma5ly profile image
Thomas Pikauli Author

Wow, thanks for your response Sung! Really appreciate it :)

Very curious what you're going to build. I'll definitely tweet codesandbox too!

Collapse
dance2die profile image
Sung M. Kim

I've never dived deep into Synthetic events as it was more of trial & error for using them.

You can see a site I built there in the Explore page (search for bunpkg 🙂)

Thread Thread
ma5ly profile image
Thomas Pikauli Author

Very cool! It's always nice if you can use your keyboard to navigate lists :)

Thread Thread
dance2die profile image
Sung M. Kim

Thanks for the suggestion Thomas~

I created an issue on the project page.

Collapse
blnkspace profile image
Aviral Kulshreshtha

The title is a meme in itself and the result is art! Love it!
Really appreciate the left-of-center, superfluous yet extremely informative approach to talking about a core subject!
Sidenote: this pushed me to see how easy Immer is maybe I won't set up ImmutableJS on my next project.

Collapse
ma5ly profile image
Thomas Pikauli Author

Thanks a lot, really appreciate it :D

I'm not super familiar with ImmutableJS, but definitely give Immer a try and see if you can get away with just using that :)

Collapse
qm3ster profile image
Mihail Malo

Some of those are really strange.
Eg if I hold backspace, it keeps counting keyDown but doesn't add even one keyPress. Whereas if I hold a letter key, it keeps spamming both?

Collapse
ma5ly profile image
Thomas Pikauli Author

I think the difference has to do with keyPress only registering actual characters like letters and numbers, while keyDown registers any key.

Collapse
qm3ster profile image
Mihail Malo

I feel like in both cases keyDown should fire once per physical press and then wait for keyUp before being able to fire again for this keycode. In nether case did it do that.

keyPress on the other hand can do whatever it damn pleases, including reacting only to characters appearing, I don't care :v