A Developer Approach to Event Handlers

2020. 7. 27.

A Developer Approach to Event Handlers

Event handlers are always used when developing UIs. Not just on the web, but native applications also use nearly identical abstractions, albeit with slight differences. As frequently as they are used, they are important, but because they are used so often, we tend to think lightly of them and habitually write poor code without much thought. Being frequently used means they constitute a large portion of the code in terms of quantity, and thus, paying a little more attention can yield significant benefits. Here's a brief summary of how to handle these event handlers effectively.

Event handlers aren't necessarily limited to DOM events. I believe the principles apply equally to event handlers used in event buses for reducing coupling between modules, RX, and various event-related patterns.

Naming

Naming often requires fresh consideration even for similar subjects in different projects. This is because the domain being handled or the metaphors used can differ from project to project. Of course, there are naming conventions that can be widely used across projects. Problems with naming can arise from these habitually used names. Such habits might stem from a team's convention, something learned from a book, or observed in others' code. Someone used to Hungarian notation might keep trying to use it, and if there's a name frequently used for a specific target, they'll type it first without considering if a better name exists. Not only is the target familiar, but the name and target are already a perfect match in the developer's mind, eliminating the need to search for a better name. One of the most common naming habits in front-end development can be found in event handlers. I myself used it habitually, and I still occasionally use it in simple test code or example code for educational or presentation purposes to aid understanding.

That is... none other than... the onEvent naming convention! Ta-da!!

I just coined the term onEvent naming convention for ease of reference. Naming the naming. (...) Experienced developers will immediately understand what this means. Let's look at an example using React, but the principle applies equally to Vue, Angular, or direct DOM API usage.

function MyFancyButton() {
  const onClick = () => { … }

  return <button onClick={onClick} />
}

The example code is a simple React component. All it does is bind a click event to a button tag, but let's imagine this simplifies more complex service code. The point I want to make actually has a greater impact in complex code. Since the required event is a click event, the event handler function bound to it was named onClick. This is what the onEvent naming convention refers to.

This naming problem also appears when the event handler function is written as an inline anonymous function.

function MyFancyButton() {
  return <button onClick={() => …} />
}

// With DOM API
button.addEventListener(‘click’, () => …, false);

The example code is too simple for the problem to be glaringly obvious, but with more complex code, readability drops sharply. The problem here is that it's not immediately clear what action should be performed when the event occurs on the DOM element or component. If the event handler for an onclick event is named onclick, or if the handler is defined inline, you can only understand what action is performed by reading the handler function's code line by line. Naming—giving a name to a target—is intended to explain what the target is by its name, making the code easier for humans to read, but the name provides no information. It's no different from naming a function 'function'.

(Information provided by the code)
click event -> execute onClick function or anonymous function

Depending on the framework, the code binding the event handler and the code defining the event handler are usually physically separated.

// …
return <button onClick={onClick} />

When encountering code like the above while reading, the process of figuring out what happens when the button is clicked is not smooth.

1. The onClick function is registered as the event handler for the click event.
2. Find the onClick function (by scrolling or searching...).
3. Read the code of the onClick function.
4. Go back to the code in step 1.
5. Re-grasp the surrounding context.

To understand what happens when the click event occurs, you first need to navigate to the code where the onClick event handler function is defined. After scrolling or searching to find the definition of the onClick function, you read the code to understand its action. After reading the code, you return to the location where you first encountered the click event. It would be great if you could immediately pick up the flow of reading the code from before navigating to the onClick function, but having been elsewhere, you reread the surrounding code to recall the flow. The example code uses only one button element and one click event, but code using various elements and events would require frantic navigation back and forth.

JSX or extended HTML syntax in Vue or Angular allows binding inline event handlers, which can reduce the travel distance, but this can significantly increase code complexity. Therefore, it's better practice to consciously define the function beforehand and only bind the function in the code that creates the HTML.

The solution to this problem is simple.

Give the event handler function a proper name that sufficiently describes what the function does. Let's assume the function previously named onClick is responsible for opening a submenu. A name like openSubMenu seems appropriate.

function MyFancyButton() {
  const openSubMenu = () => {…}
  return <button onClick={openSubMenu} />
}

By simply giving the event handler a proper name, the flow of reading the code becomes much simpler:

1. When a click event occurs, the openSubMenu function is executed (click event -> open submenu).

Unless you need to modify the code for opening the submenu, there's likely no need to examine the openSubMenu function itself. At the time of writing the code, the developer knows exactly what happens when the event occurs, so an event handler name like onClick might seem perfectly adequate for understanding the code. However, when rereading the code after some time has passed, or when another collaborating developer reads the code, the name provides almost no information, leading to more time spent reading the code. The onClick function is naming on the same level as naming a function 'function'.

Occasionally, such as when the action corresponding to an interaction event varies depending on the component's state, the onEvent naming convention might seem most appropriate for the event handler's name. Except for those cases, it's best to avoid it if possible.

As various statistics show, naming is often considered the most challenging part of development. The fact that developers find it difficult, even though they could just name things carelessly, indicates they understand the value of naming. Naming is difficult precisely because its value is significant. One good name is worth ten comments.

The Role of Event Handlers

So far, I've explained the importance of giving event handlers clear names.

It's better to think of an event handler as a kind of holder capable of receiving event messages, rather than as a function that embodies the action itself. The event handler is merely a conduit connecting an externally delivered event message to the component's interface, its methods.

Consider the specification for a UI component under development:

  • The component contains a button element.
  • Clicking the button element should perform action A.

Faced with such a spec, the development process usually follows this thought flow:

  1. Create the button element.
  2. Define an event handler for the button that performs action A.
    • Since it might get long, define it as a separate function and bind it, rather than inline.

However, as mentioned earlier, the event handler is just a holder for receiving event messages. The priority should be the component's behavior. When faced with such a spec, the work should proceed with the following thought flow:

  1. The component needs action (behavior, responsibility) A.
  2. Decide on a method name that describes action A.
  3. Write test cases to verify if the method adequately performs action A.
  4. Write the code for the method.
  5. Bind the written method as the event handler for the button.

Suddenly, testing makes a cameo. Huh? An event handler is an internal implementation, should we write test cases for it? (Reference: [A Developer's Approach to Efficient Testing]) However, an event handler is closer to an external interface. Only the method of executing the method differs. The received message is the event. Conversely, the module has an external interface, which happens to be used by (or in) the event handler.

Defining an event handler shouldn't start with the handler function itself. Instead, the correct mindset is that the component (module) has a method (function) fulfilling a role, and it just so happens that this method is executed when a specific event is received. Think of the event handler as merely a bridge connecting interaction event messages to the module's behavior (method).

Literally, Event Handlers

What about methods that require the event object passed as an argument to the event handler? It seems obvious that the event handler and the module's behavior cannot be clearly separated, strongly constraining it as a special type of function—an event handler. Executing the method independently would be meaningless.

The following code is a simple example of showing a context popup using the mouse position.

const openPopupMenu = (ev) => {
 const {clientX, clientY} = ev;

 this.popup.style.left = `${clientX}px`;
 this.popup.style.top = `${clientY}px`;
 this.popup.display = ‘block’;
}

popupButton.addEventListener(‘click’, openPopupMenu);

The onClick event handler above currently has a strong dependency on the event object. In such cases, can the handler and the module's method not be separated? No, they can be separated even in this case.

const openPopupMenu = (x, y) => {
 popup.style.left = `${x}px`;
 popup.style.top = `${y}px`;
 popup.display = ‘block’;
}

popupButton.addEventListener(‘click’, ({clientX, clientY}) => openPopupMenu(clientX, clientY));

The inline event handler acts as a bridge, receiving the event triggered by interaction, selecting the internal method (openPopupMenu), and executing it appropriately. It can process data from the event object to pass information like x and y, and interaction control can also be handled within the event handler function. For instance, calling methods like stopPropagation() or preventDefault() on the event object. This represents the true role of an event handler: receiving an event and selecting the appropriate module action to execute. The component originally had the functionality (responsibility) to open a popup at a specific location, and this functionality was triggered by an event. This is how the event handler and the module's method can be distinguished.

The event handler in the example code was simple enough to be defined inline. However, if the event handler needs to branch to different methods based on specific conditions or execute multiple methods sequentially, it should be extracted into a separate function with a name that encompasses these actions, raising the level of abstraction. When using anonymous functions for inline definitions, it's best to limit them to using the event object, interpreting it to create data for the module's method, and executing a single method. The role of the event handler is to interpret the event message and translate it into a method execution.

Testing Event Handlers

Thinking of event handlers and module methods separately can also facilitate testing. The event handler merely selects the module's method and interprets the event object to pass the necessary data to this method. For the openPopupMenu() function, it requires arguments x and y of type number. This method can be executed either by code or by an event. In this case, unit testing is sufficient by only writing test cases for executing the openPopupMenu() method directly via code.

The logic of the event handler should be kept as simple and light as possible, so the handler itself is not tested. Unit testing event handlers is often just mocking, which doesn't guarantee the actual interaction environment. Complex interactions become increasingly difficult to cover with unit tests. Writing test cases can be time-consuming, and even if tests are written, their high complexity often makes them difficult to maintain. Drag and drop is truly...

If the event handler's functionality is minimized, testing might only involve checking if the handler is executed when the event occurs, which seems more appropriate for browser development test code. If the event handler is kept lean, merely selecting a specific method and passing data to it, testing would boil down to verifying that a specific function should be executed upon receiving an event message. Is this really necessary to test? This is an example of distinguishing between testable (efficient) and untestable (inefficient) things, as mentioned in [A Developer's Approach to Efficient Testing]. Areas that are untestable or where testing is inefficient should be separated (isolated) into distinct layers to minimize their impact and keep them light and simple. Focus testing efforts on the areas that can be tested efficiently. If thorough testing of interaction-driven event handlers is absolutely necessary, E2E testing might be the answer.

However, this testing strategy is primarily valid when writing library or framework code. When developing typical web services, components are usually built using popular frameworks. In such cases, accessing the methods or functions used in a component's event handlers from test case code can be difficult. Therefore, unit tests often have to be written by mocking events. While E2E testing is still recommended here, if unit tests are required due to circumstances, using a library like testing-library is advisable.

In projects of a certain scale, different testing strategies should be adopted for each layer depending on the framework, domain, development environment, etc. Some areas are adequately covered by unit tests, while others benefit more from E2E tests, or perhaps visual testing using tools like Storybook. Suddenly, this has become all about testing. Let's get back on track and wrap up.

Wrapping Up

Overall, I've summarized how developers should approach the abstraction known as event handlers (a type of function/method execution).

Development patterns utilizing events are used not only for interaction but throughout applications, as they effectively reduce coupling between modules. Especially in the JavaScript ecosystem, most solutions for asynchronous processing are based on event-related patterns. Promises are no exception. Since they generally share similar abstractions, understanding how to handle event handlers effectively is crucial.

Regarding testing, my personal opinions might be overly represented. Please consider them the biased views of a developer with an extremely pragmatic TDD inclination.

I hope this article encourages you to think more deeply about event handlers and write better code.

♥ Support writer ♥
with kakaopay

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

shiren • © 2025Sungho Kim