Events in Javascript
Events in JavaScript: A Comprehensive Guide

Sky always blue
Events in JavaScript empower developers to respond to user interactions, browser actions, and other asynchronous occurrences with precision and finesse. In this comprehensive exploration, we'll delve into the fundamental concepts of events, their types, and how they work. We'll also discuss how to attach event listeners, handle events, and manage event propagation. From basic click events to more complex custom events, we'll equip you with the knowledge and skills needed to bring your web applications to life.
High-level DOM Event concepts

Event
To understand this picture, we'll need to go through some basic terminology, but first of all, what is an event?

Browser events (finished loading a page, DOM finished loading)
Network events (fetch event, server-side events)
User events (click, mouse move, etc)
Timer events (setTimeOut,setInterval, etc)
Event Handling at High-level
The browser execution environment, at its core, only handles one function at a time

Example of event flow

Observer patterns with events
Real-life

Observer Pattern defined: the class diagram
interface Observer {
update:(tempature:number,humidity:number,pressure:number)=>void;
}
interface Subject {
registerObserver:(observer:Observer)=>void;
removeObserver:(observer:Observer)=>void;
notifyObserver:()=>void;
}
class WeatherData implements Subject {
private observers:Observer[];
private tempature:number = 0;
private humidity:number = 0;
private pressure:number = 0;
constructor(){
this.observers = [];
}
public registerObserver(o:any) {
this.observers.push(o);
}
public removeObserver(o:Observer) {
this.observers.filter((observer)=>observer !== o);
}
public notifyObserver() {
this.observers.forEach((observer)=>observer.update(this.tempature,this.humidity,this.pressure));
}
public measureChange() {
this.notifyObserver();
}
public setMeasurements(tempature:number,humidity:number,pressure:number) {
this.tempature=tempature;
this.humidity=humidity;
this.pressure=pressure;
this.measureChange();
}
}
Observer patterns are the key to understanding how events are subscribed in JS
class Observer {
constructor() {
this.observers = new Map();
}
addObserver(key,observerFn) {
if(!this.observers.has(key)) {
this.observers.set(key,new Set())
}
this.observers.get(key).add(observerFn)
}
removeObserver(key, observerFn) {
const observerCollections = this.observers.get(key);
if (observerCollections) {
observerCollections.delete(observerFns);
if (observerCollections.size === 0) {
this.observers.delete(key);
}
}
}
notifyObservers(key, data) {
const observerCollections = this.observers.get(key);
if (observerCollections) {
observerCollections.forEach(observer => {
observerCollections(data);
});
}
}
}
Event Target
An event target is any object that implements the EventTarget interface (eg window, Element and XMLHttpRequest)
An event target can be the target of events and can have event listeners added and removed from them

Phrase
When an event target participates in a tree 🌲 the event flows through the tree in three phases:
Capture listeners are called on the way down from the root to the target
Target listeners are attached to the target (Where the event starts from the tree)
Bubble listeners are called on the way up from the target towards the root.
The bubble phase will only occur if
event.bubblesistrue💡Most events are bubble, except focus events and other rare event listeners that we'll meet later and by default capture is turned off; we need to set it to true to activate it

Explain it in simple terms

event target vs eventCurrentTarget
A handler on a parent element can always get the details about where it happened
The most deeply nested element that caused the event is called a target element ( event target )
event. target is the target element that initializes the events and doesn't change through the bubble process
event.currentTarget is the current element (which usually changes through the bubble process )
How many ways can we attach events to an HTML element?
HTML
<!-- In HTML or (React way) -->
<button
onclick="console.log(this.event)"
type="button">
Call to action
</button>
Query a document element and assign property or addEventListener
const button = document.querySelector("button");
if(button === null ) {
throw new Error("Unable to query the button");
}
//Object property event handler
button.onclick = function onClick(e)=> {
console.log(e);
}
//Attach an event listener to button
button.addEventListener("click",(e)=> {
console.log("onClick",e);
}
Event Listener Mechanism

/* Best is to use addEventListener */
/* It allows multiple event listeners to be added */
button.addEventListener("click",onClick);
button.addEventListner("click",onClick2);
/* A lot control over binding (capture) */
button.addEventListner("click",onClick,{capture:true });
/* Some events (system events) do not have corresponding HTML
attribute or object property event handler */
window.addEventListener("DOMContentLoaded" , function log () {
console.log('DOMContentLoaded');
});
Remove Event Listener
Event
Events name
Listen to these events using addEventListener() or by assigning an event listener to the oneventname property of this interface.
https://developer.mozilla.org/en-US/docs/Web/API/Element#events
https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L8267
Events interface
The Event interface represents an event that takes place in the DOM.

More specific interface
To properly handle the event, we want to know more about what's happened. Not just a "click" or a "key-down", but what were the pointer coordinates? Which key was pressed? And so on
When an event happens, the browser creates an event object, puts details into it and passes it as an argument to the handler
<input type="button" value="Click me" id="elem">
<script>
elem.onclick = function(event:any ??? ) {
// Some of the generic event that we can access
// event.target || event.currentTarget || event.preventDefault();
alert(event.type + " at " + event.currentTarget);
alert("Coordinates: " + event.clientX + ":" + event.clientY);
};
</script>
Click is indeed a MouseEvent interface
The MouseEvent interface represents events that occur due to the user interacting with a pointing device (such as a mouse). Common events using this interface include click, dblclick, mouseup, mousedown.


Event with Typescript
We also have an event hierarchy, just like we have with the DOM element. We'll have a base Event that shares all the common properties to other events
More specific types include:
UIEvent (any sort of user interface event)
MouseEvent (An event triggered by the mouse, such as click)
TouchEvent (A touch event on mobile)
WheelEvent (An event triggered by rotating the scroll wheel)
KeyboardEvent(A key press)
const mouseDown = (el:MouseEvent)=> {
el.clientX, el.clientY // correct
//currentTarget has an interface EventTarget,this is so generic
//and if we re gonna have classList property it should be a HTML element
const target = el.currentTarget as HTMLElement; //
target.classList.add("dragging");
}
Stop bubbling (stop propagation)
A bubbling event goes from the target element straight up. Normally it goes upwards until <html>, reaches to document object, and some events even reach it window, calling all handlers on the path.
But any handler may decide that the event has been fully processed and stop the bubbling.
The method for it is event.stopPropagation().
Stop multiple event handles with e.stopImmediatePropagation
Event delegation
Capturing and bubbling allow us to implement one of the most powerful event-handling patterns, called event delegation.
Browser default actions
Some of the actions automatically trigger an event for us
A click on a link navigates to a different page ( it will cause the browser to refresh the page)
A click on the form submit button initiates its submission to the server
Pressing the mouse button over a text will select the text
JS gives us the option to stop these automatic events
const anchor = document.querySelector("a");
anchor.addEventListener("click",(e)=>{
e.preventDefault();
});
Create an image gallery where the main image changes by clicking on a thumbnail
Technically, by preventing default actions and adding JavaScript we can customize the behavior of any elements. For instance, we can make a link <a> work like a button, and a button <button> behaves as a link (redirect to another URL or so).
But we should generally keep the semantic meaning of HTML elements. For instance, <a> should perform navigation, not a button.
Besides being “just a good thing”, that makes your HTML better in terms of accessibility.
Custom Event
In FE worlds, where HTML is not enough, we create our components (toggle button, menu, tree, etc.). Same for the event; we can create our own event

type: event type, a string like "click" or our own like "my-event"
options: the object with two optional properties
bubbles: true/false if true, then the event bubbles
cancelable: true/false if true, then the default action may be prevented
By default: bubbles and cancelable are false
Dispatch an event
After an event is created, we have to dispatch it through an element
const button = document.querySelector("button");
if ( button === null ) { return ; }
button.addEventListner("hello",()=>{console.log("Run my hello event");})
const event = new Event("hello");
button.dispatchEvent(event);
Remind me of dispatch in reducer (reactJS)
Key Event
https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L25369
interface Element extends Node, ARIAMixin, Animatable, ChildNode {
addEventListener(type: string, listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions): void;
}
type EventListenerOrEventListenerObject = EventListener | EventListenerObject;
interface EventListener {
(evt: Event): void;
}
interface EventListenerObject {
handleEvent(object: Event): void;
}
When you query the element then if TS infers it as Element which is what it do when you query by className or ID

Only when we know the exact element we query then we can get the benefit

Every single element HTMLButtonElement,Element,HTMLAnchorElement has a specific addEventListener
<script>
window.addEventListener("keydown", event:KeyEvent) =>
{
if (event.key == "v") {
document.body.style.background = "violet";
}
}
);
window.addEventListener("keyup", event:KeyEvent) => {
if (event.key == "v") {
document.body.style.background = "";
}
}
);
//Look at key combinations
window.addEventListener("keydown", event:KeyEvent) => {
if(event.key =="" & event.ctrlKey) {
console.log("Continuing");
}
})
</script>
The DOM node where a key event originates depends on the element that has focus when the key is pressed. Most nodes cannot have focus unless you give them a tabindex attribute, but things like links, buttons, and form fields can. When nothing in particular has focus, document.body acts as the target node of key events
https://stackoverflow.com/questions/31636337/javascript-keydown-event-wont-fire-on-paragraph
Pointer Event
Pressing a mouse button causes a number of events to fire. The "mousedown" and "mouseup" events are similar to "keydown" and "keyup" and fire when the button is pressed and released
mousedown/mouseup
mouseover/mouseout (hover)
mousemove
click
dblclick
context menu (trigger when the right button is clicked)
Events order
The user can trigger multiple events ( mouse down -> mouse up -> click)
MouseEvent
To get precise information about the place where a mouse event happened, you can look at some of these properties
offSetX/offSetY: event's coordinates ( in pixels ) relative to the parent container
clientX/clientY: coordinates relative to the top left corner for the window
pageX/pageY: coordinates relative to the top left corner of the entire page (when the window has been scrolled)

Mouse move
When you click on the element and drag it, a "mousedown" event is fired
Every time a mouse pointer moves, a "mousemove" event is fired. This event can be used to track the position of the mouse.
When you release the mouse, a "mouseup" event is fired. And possibly we want to cancel the "mousemove" event
Touch events
Pretty similar to the mouse move event. We have "touchstart", "touchmove" and "touchend"
Scroll events (window event)
Whenever an element is scrolled, a "scroll" event is fired on it
We use % rather than px as a unit when setting the width so the element is sized relative to the page width
Focus Events
When an element gains focus, the browser fires a "focus" event on it. When it loses focus, the element gets a "blur" event
Load event
When a page finishes loading, the "load event" fires on the window and the document body objects. This is mainly used when we want to fire some actions that require the whole document to have been built
Elements such as images or script tags that load an external file also have a "load" event that indicates the files they reference were loaded. Like focus-related events, loading events do not propagate
When a page is closed or navigated away from (for example, by following a link), a "before unload" event fires. The main use of this is to prevent users from accidentally losing work by closing a document.
Media event
Limit event streams with debounce
Some types of events have the potential to fire rapidly, many times in a row (the "mousemove" and "scroll" events, for example)
If you do need to do something nontrivial in such a handler, you can use setTimeout to make sure you're not doing that often. This is usually called debouncing the event. There are several slightly different approaches to this

React examples
Because of the nature of single-threaded language like JS, bringing the concept of that to ReactJS is the same thing
(() => {
const toggleVisibility = () => {
//detect if the page has been scroll to the bottom
};
const debounceToggle = debounce(toggleVisibility, 100);
window.addEventListener("scroll", debounceToggle);
return () => window.removeEventListener("scroll", debounceToggle);
}, [detectDevice]);
const [text,setText] = React.useState("");
//This also has a performance issue when users typing quick , the
//UI will be hard to follow :) the solution is useDefferedValue
return (
<main>
<input
type="text"
value={text}
onChange={({currentTarget})=>setText(currentTarget.value)} />
{
[...Array(30000)].map(()=>
<p> {text} </p>
}
</main>
)
Summary
Pressing a key fires "keydown" and "keyup" events with focus mode on element
Pressing a mouse button fires "mousedown", "mouseup" and "click" events. Moving the mouse fires "mousemove" events
Touchscreen interaction fires "touchstart","touchmove" and "touchend" events
Scrolling can be detected with "scroll" event
Focus can be detected with "focus" events and "blur" events
When the document finishes loading, a "load" event fires on the window
Events and Event loop
In the context of events, the events can only be processed when nothing is running, which means that if the event loop is tied up with other work, any interaction with the page (which usually happens through the events) will be delayed until there's time to process it. So if you schedule so much work, either with long-running event handlers or with a lot of short-running ones, the page will become slow and cumbersome to use
Event Loop Basics
JavaScript runs on a single thread (one call stack).
The event loop decides what to run next.
Execution order:
Run synchronous code (everything in the call stack).
Run all microtasks (from the microtask queue).
Run the next macrotask (from the macrotask queue).
Repeat.
🔹 Macrotasks (a.k.a. Tasks)
Macrotasks are scheduled to run after the current synchronous code finishes.
Examples:
setTimeoutsetIntervalsetImmediate(Node.js only)I/O callbacks (Node.js)
User events → e.g.
onclick,oninput,onscrollhandlersNetwork events → e.g.
fetch().then’s network callback (but careful: see below)UI rendering events (in browsers)
📌 Each macrotask is executed one at a time, and after it finishes, the event loop processes all microtasks before moving to the next macrotask.
Important Gotcha with Network Events
The “network finished downloading” signal itself is a macrotask.
But if you use
fetch().then(...), the.thencallback is scheduled as a microtask, because it’s chained off aPromise.
fetch("data.json").then(() => {
console.log("Microtask (Promise.then)");
});
setTimeout(() => {
console.log("Macrotask (setTimeout)");
}, 0);
Order:
Network finishes → enqueues macrotask (resolve fetch’s promise).
The promise resolution itself → runs as a microtask, before the timeout.
🔹 Microtasks (block render)
Microtasks are scheduled to run immediately after the current code execution, before the event loop continues to the next macrotask.
Examples:
Promise.then/Promise.catch/Promise.finallyqueueMicrotaskMutationObserver
📌 All microtasks are executed before moving on to the next macrotask.
- 60 FPS (frame per second)💡
Microtasks (
Promise.then,queueMicrotask) run before rendering, so if you chain too many → you block rendering again.Macrotasks (
setTimeout,MessageChannel) let the browser paint in between → better for long work.
Scenarios
Because the browsers are trying to render within 16 ms, the event loop often asks, "Is rendering required?" for every 16 ms. Executing the macrotask and all of its related microtasks takes much more than 16 ms. In this case, the browser won't be able to render the page at the target frame rate, and the UI won't update. If we have some animations running on the page, users will feel the page is slow and unresponsive.
An example of macrotasks
JS is a single-threaded execution model and can handle only one at a time
Let's have an example
We have global JS code to execute the mainline
We have two buttons and two clicks to the buttons, one for each button

An example of both microtask and macrotasks
Macrotasks are kinds of stuff like promise and DOM manipulations
<script>
const firstButton = document.getElementById("firstButton");
const secondButton = document.getElementById("secondButton");
firstButton.addEventListener("click",()=> {
Promise.resolve().then(()=>{
});
/* click will run after 5ms quick users has clicked on it */
})
secondButton.addEventListener("click",()=> {
Promise.resolve().then(()=>{
});
/* click will run after 8ms quick users has clicked on it */
})
/* Code that run for 15s */
</script>


Timers
Timers give us the ability to break long-running tasks into smaller tasks that won't block the event loop
Timers aren't defined in JS itself; instead, they're provided by the host environment (browser or nodeJS)
setTimeout (repeat calling a function after ms time)
setInterval(calling a function every ms time)

Unlike the setTimeout function, which expires only once, the setInterval function fires until we explicitly clear it. So, at around 20 ms, another interval fires. Normally, this would create a new task and add it to the task queue. But this time, because an instance of an interval task is already queued and awaiting execution, this invocation is dropped. The browser won’t queue up more than one instance of a specific interval handler (probably for optimization)
set timeout vs setInterval
setTimeout(function repeatMe() {
/** doing some works **/
setTimeout(repeatMe,10);
},10);
setInterval(()=>{
/** doing some works **/
},10);
Notably, the setTimeout variant of the code will always have at least a 10ms delay after the previous callback execution (depending on the state of the event queue, it may end up being more but never less)
🔹 What React Scheduler Gives You
React doesn’t just “set state.” It has a whole scheduling system (Fiber + Scheduler) that is designed to keep apps responsive.
1. Batching
React batches multiple state updates into one render, saving work:
// In React 18, both updates batch automatically
setCount(c => c + 1);
setCount(c => c + 1);
👉 You only get 1 render, not 2.
2. Prioritization
React assigns different priorities to updates:
High priority → user typing, button clicks (must show immediately).
Low priority → background data fetching, logging, etc.
This means React can delay non-urgent work and keep the UI responsive.
3. Interruptible Rendering (Concurrent Mode)
React Fiber breaks rendering into small chunks.
If rendering takes too long, React can pause, let the browser handle a frame, and resume later.
👉 This avoids blocking the main thread for >16ms.
4. Consistency with the Event Loop
React’s scheduling system works with the event loop:
Uses macrotasks (
MessageChannel,setTimeout) and microtasks (Promise.then) smartly.Ensures rendering doesn’t starve browser painting.
Lets React do work just before rendering (
requestAnimationFrame) or in idle time (requestIdleCallback).
5. Cross-platform abstraction
React’s scheduler works in:
Browsers (DOM rendering)
React Native (mobile rendering)
React Three Fiber (WebGL rendering)
You don’t have to think about microtask/macrotask juggling yourself.
Streaming (handle bit by bit)
JS is single-threaded and if we have many event listeners or some heavy computing on JS, the browser may stutter or seem to hang because all updates to the rendering of a page are suspended while JS is executing
In these situations, timers can come to the rescue and become especially useful because they are capable of effectively suspending the execution of a piece of JS until a later time
<body>
<table>
<tbody></tbody>
</table>
</body>
<script>
// This piece of will run long time in callstack
// tr.appendChild just a function that manipulate the DOM
// not in queue
const tbody = document.querySelector("tbody");
for (let i = 0; i < 20000; i++) {
const tr = document.createElement("tr");
for (let t = 0; t < 6; t++) {
const td = document.createElement("td");
td.appendChild(document.createTextNode(i + "," + t));
tr.appendChild(td);
}
tbody.appendChild(tr);
}
</script>

- Because of the single-threaded execution model, tasks are processed one at a time, and after a task starts executing, it can’t be interrupted by another task.
In this example, we're creating a total of 240.000 DOM nodes, populating a table with 20.000 rows of 6 cells. This will likely hang the browser

It's very similar to this mindset when handling streaming data on NodeJS

Summary
Events in JS are such an important topic in JS land. And in their simple form, events are still quite hard to wrap my heads around, and it's not obvious how events work in JS. More than that, events can be triggered multiple times, and we have to handle our code well to provide smooth interactions on the web because we already know that JS is single-threaded and firing multiple events can be a dead end for our customers. And you already know why, hopefully after this blog. That's it, folks. Until next time, see you.




