JavaScript and TypeScript course Β· Module 8: Events and the DOM

Best Practices for Event Handling

5 min read Β·

Key principles for effective and safe event management in JavaScript applications.

"Most failures in the control center don't come from hard problems," says Ray Arnold, going through the incident log. "It's the little things: a sensor connected to a device that isn't there, an alarm nobody switched off, a lock put in place just in case." This lesson gathers those little things into rules worth turning into reflexes. You've already seen each of them in action, now we're putting them into a single checklist.

Rules for Good Code

The first rule protects you from the most common beginner mistake. Before you attach a listener, make sure the element exists at all.

// 1. Always check if the element exists
const button = document.getElementById('my-button');
if (button) {
  button.addEventListener('click', handleClick);
} else {
  console.warn('Button with ID "my-button" was not found');
}

The getElementById() method returns null when there is no element with the given ID on the page, for example because of a typo or a script that runs before the HTML has loaded. Calling addEventListener() on null throws a TypeError and stops the rest of the script. A shortcut would be button?.addEventListener(...), but I prefer an explicit condition with a warning, because a silent ?. hides the typo instead of reporting it.

The second rule concerns elements that come and go. Instead of attaching a listener to every button, listen on their common parent.

// 2. Use delegation for dynamic elements
const parent = document.getElementById('dynamic-content');
parent.addEventListener('click', (e) => {
  if (e.target.matches('.dynamic-button')) {
    handleDynamicButtonClick(e);
  }
});

The e.target.matches('.dynamic-button') condition only lets through clicks on buttons with that class, so new buttons work without any extra listeners. Remember from the lesson on delegation that matches() checks only the clicked element. If there's an icon inside the button, closest() is the safer choice.

The third rule is cleanup. A component that attaches a listener should also be able to remove it.

// 3. Remember to clean up event listeners
class ParkComponent {
  constructor(element) {
    this.element = element;
    this.handleClick = this.handleClick.bind(this);
    this.init();
  }

  init() {
    this.element.addEventListener('click', this.handleClick);
  }

  handleClick(e) {
    console.log('Park component clicked');
  }

  destroy() {
    this.element.removeEventListener('click', this.handleClick);
  }
}

The key line is the one with bind(this) in the constructor. The bind() method creates a new function with this permanently attached, and we store it as this.handleClick. Thanks to that, init() and destroy() use the same reference, so removal works. Without bind(), this inside handleClick() would point to the element, not the component.

The fourth rule is about blocking the browser's default behaviors.

// 4. Use preventDefault() consciously
const form = document.getElementById('contact-form');
form.addEventListener('submit', (e) => {
  e.preventDefault(); // Stops the default form submission

  // Custom validation and submission logic
  if (validateForm()) {
    submitFormAjax();
  }
});

Here preventDefault() has a specific reason: it stops the standard submission and page reload, because our own code will handle the form. Don't call it just in case. On a link it blocks navigation, with touch it can stop scrolling, and in a passive listener it is simply ignored.

The fifth rule: register listeners in one style. The onclick property stores only one function, while addEventListener() adds more listeners.

// onclick stores only one function - the second one overwrites the first
alarmButton.onclick = () => console.log('Alarm in sector A');
alarmButton.onclick = () => console.log('Alarm in sector B');

// addEventListener adds more listeners
alarmButton.addEventListener('click', () => console.log('Notify security'));
alarmButton.addEventListener('click', () => console.log('Write to the log'));

What does a single click print? Three lines: "Alarm in sector B", "Notify security", and "Write to the log". The sector A message is gone without a trace, because the second assignment overwrote it. That's why you should stick to addEventListener() throughout the project, since it never takes anything away from anyone.

A Short Checklist to Finish

Besides the five rules from the code, a few more are worth remembering:

  • Keep listeners you plan to remove in a named function or a variable, because you can't remove a function without a reference through removeEventListener()
  • Limit frequent events, such as scroll, resize, or input, with throttling or debouncing
  • Add the { passive: true } option to touch and mouse wheel listeners that don't call preventDefault()
  • Use <button> for clickable elements, not <div>

The last point is easy to overlook. A button gets keyboard support from the browser for free: pressing Enter or the space bar sends the same click event as the mouse. A clickable div would need that support written by hand, and usually somebody forgets about it.

My advice: before you hand in your code, go through this list like a morning fence inspection. It takes a minute and saves hours of hunting for bugs. In the sandbox below you'll see good and bad versions of the same solutions side by side. In the next lesson we'll move on to advanced techniques, starting with touch gestures.

Remember: there is no such thing as a small oversight in the park, and every unhandled null can open a gate that nobody is guarding.

Code for this lesson: index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event Handling Best Practices</title>
    <style>
        body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
        .demo-box { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; }
        .good { border-left: 4px solid #27ae60; }
        .bad { border-left: 4px solid #e74c3c; }
        button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; margin: 5px; }
        code { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px; }
    </style>
</head>
<body>
    <h1>Event Handling Best Practices</h1>

    <div class="demo-box good">
        <h3>Good: Checking that the element exists</h3>
        <button id="good-btn">Click me</button>
        <p>Code uses safe pattern <code>element?.addEventListener</code></p>
    </div>

    <div class="demo-box good">
        <h3>Good: Event delegation</h3>
        <div id="dino-container">
            <button class="dino-btn" data-dino="trex">T-Rex</button>
            <button class="dino-btn" data-dino="velociraptor">Velociraptor</button>
        </div>
    </div>

    <div class="demo-box good">
        <h3>Good: Cleaning up listeners</h3>
        <button id="cleanup-test">Test Cleanup</button>
        <p id="cleanup-status">Status: Inactive</p>
    </div>

    <script>
        console.log('Good practices demo');

        // GOOD PRACTICE 1: Checking that it exists
        const goodBtn = document.getElementById('good-btn');
        if (goodBtn) {
            goodBtn.addEventListener('click', () => {
                console.log('Element exists - safe click');
                alert('Safe click!');
            });
        }

        // GOOD PRACTICE 2: Event Delegation
        const dinoContainer = document.getElementById('dino-container');
        dinoContainer.addEventListener('click', (e) => {
            if (e.target.classList.contains('dino-btn')) {
                const dinoName = e.target.dataset.dino;
                console.log(`Delegated click: ${dinoName}`);
                alert(`You selected: ${dinoName}`);
            }
        });

        // GOOD PRACTICE 3: Proper Cleanup
        class ComponentWithCleanup {
            constructor(element) {
                this.element = element;
                this.handleClick = this.handleClick.bind(this);
                this.init();
            }

            init() {
                this.element.addEventListener('click', this.handleClick);
                console.log('Component initialized');
            }

            handleClick() {
                console.log('Component clicked');
                document.getElementById('cleanup-status').textContent = 'Status: Active';
            }

            destroy() {
                this.element.removeEventListener('click', this.handleClick);
                console.log('Component cleaned up properly');
                document.getElementById('cleanup-status').textContent = 'Status: Cleaned up';
            }
        }

        const cleanupBtn = document.getElementById('cleanup-test');
        const component = new ComponentWithCleanup(cleanupBtn);

        // Simulated cleanup after 5 seconds
        setTimeout(() => {
            component.destroy();
        }, 5000);

        // GOOD PRACTICE 4: Use preventDefault consciously
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.key === 's') {
                e.preventDefault();
                console.log('Prevented default save, implemented custom logic');
                alert('Park state saved!');
            }
        });

        console.log('All good practices implemented');
    </script>
</body>
</html>

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Why is it worth checking that an element exists before calling addEventListener?

  2. 2. Why do we call e.preventDefault() in a form's submit event handler?

Hands-on tasks in the game

  • Click in order

    Arrange the event-delegation condition that checks the clicked element:

Useful articles