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")}
/>
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 }]
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 };
};
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})
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>
);
});
And in our return
we can add the items to our <ul />
.
<ul>{counts}</ul>
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;
}
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)
};
}
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 }];
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);
});
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>
);
});
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.
Top comments (10)
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...)
Wow, thanks for your response Sung! Really appreciate it :)
Very curious what you're going to build. I'll definitely tweet codesandbox too!
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
๐)Very cool! It's always nice if you can use your keyboard to navigate lists :)
Thanks for the suggestion Thomas~
I created an issue on the project page.
Make lists/versions navigatable using keyboard #8
Suggested by Thomas Pikauli.
dev.to/ma5ly/comment/8dge
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.
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 :)
Some of those are really strange.
Eg if I hold backspace, it keeps counting
keyDown
but doesn't add even onekeyPress
. Whereas if I hold a letter key, it keeps spamming both?I think the difference has to do with
keyPress
only registering actual characters like letters and numbers, whilekeyDown
registers any key.I feel like in both cases
keyDown
should fire once per physical press and then wait forkeyUp
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