Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active April 12, 2024 09:54
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save WebReflection/35ca0e2ef2fb929143ea725f55bc0d63 to your computer and use it in GitHub Desktop.
Save WebReflection/35ca0e2ef2fb929143ea725f55bc0d63 to your computer and use it in GitHub Desktop.
The `handleEvent` ASCII doodle

About This Gist

This gist summarizes the handleEvent(event) pattern features, something standard, something described by me many times, written in my tiny book, tweeted about, and yet it's something most Web developers ignore.

The handleEvent ASCII Doodle

                  ┌---------------------------------┐
var handler = {   | any object that inherits or     |
                  | directly implements this method |
                  | can be used as event listener   |
        ┌---------┴---------------------------------┘
        ▼                       ┌----------------------------┐
  handleEvent({                 | the event is a regular one |
    type, ◀--------------------┤ with type, preventDefault()|
                                | and all other event things |
                                └----------------------------┘
                                ┌----------------------------┐
                                | like every other event, it |
    currentTarget ◀------------┤ contains the node used to  |
                                | add the event listener     |
                                └----------------------------┘
  }) {                          ┌----------------------------┐
                                | the method is called via   |
    this === handler; ◀--------┤ handler.handleEvent(event) |
                                | so the context is handler  |
                                └----------------------------┘
  }         ┌-------------------------------------------┐
}:          | the node will be the currentTarget,       |
            | which could be different from the target, |
            | 'cause the target is the clicked element, |
            | as example, while currentTarget is always |
            | where the listener was attached to.       |
  ┌---------┴-------------------------------------------┘
  ▼
node.addEventListener('click', handler);

 ┌---------------------------------------------┐
 | like it is with exact same functions,       |
 | it doesn't matter if you add multiple time  |
 | exact same handler, as this will be ignored |
 └--------------------------------┬------------┘
                                  ▼
node.addEventListener('click', handler);

The Features

The nitty-gritty of this pattern is that:

  • you don't need arrow functions or .bind(...)
  • it works since year 2000 in every browser (20+ years rock solid)
  • it's much better in terms of memory usage, compared to bound methods or trapped arrows
  • it prevents the classic arrow or .bind(...) footgun: you can always remove a listener via type and the handler
  • it's usable in all my FE libraries: you just pass onclick=${this}, as example, and that's it

Live Test

var handler = {
  handleEvent({type, currentTarget}) {
    console.log(type, this === handler);
    currentTarget.removeEventListener(type, this);
  }
};

document.addEventListener('click', handler);

// try to add it twice or more, only one click happens
document.addEventListener('click', handler);

// you can always remove the listener too
// document.removeEventListener('click', handler);

handleEvent & Hooks

When hooked functions are used as listeners, the library needs to remove the previous listener and add the updated one.

This can be expensive and slow, as it touches the DOM per each update. However, libraries like lighterhtml, as example, won't touch the DOM if the listener is the same as it was before, so that hooks can be assigned right away to a JS object, instead of passing through DOM opertaions.

The following counter example is live in CodePen.

import {render, html} from 'lighterhtml';
import {define, useRef, useState} from 'hooked-elements';

const useClickHandler = ([count, update]) => {
  const {current} = useRef({
    handleEvent({currentTarget}) {
      this[currentTarget.dataset.action]();
    }
  });
  // attach anything to this object (it's fast and cheap)
  // instead of adding/removing listeners each time (it's slow)
  current.value = count;
  current.dec = () => update(count - 1);
  current.inc = () => update(count + 1);
  return current;
};

define("my-counter", element => {
  const handler = useClickHandler(useState(0));
  render(element, html`
    <button class="large btn" data-action=dec onclick=${handler}>-</button>
    <span class="large span">${handler.value}</span>
    <button class="large btn" data-action=inc onclick=${handler}>+</button>
  `);
});

handleEvent & React

Following a React based example with useEffect too.

import React, { useRef, useEffect, useState } from "react";

export default function Box() {
  const [focused, setFocused] = useState(false);
  const { current: clickRef } = useRef({ handleEvent });
  clickRef.setFocused = setFocused;

  useEffect(() => {
    document.addEventListener("click", clickRef);
    return () => {
      document.removeEventListener("click", clickRef);
    };
  }, []);

  return (
    <div
      style={{
        backgroundColor: focused ? "green" : "red",
        height: "50px",
        width: "50px"
      }}
      ref={clickRef}
    />
  );
}

function handleEvent({ target }) {
  const isFocused = this.current.contains(target);
  this.setFocused(isFocused);
}

The hookedElements equivalent would be written as such:

import {define, useRef, useEffect, useState} from 'hooked-elements';

define(".box", element => {
  const [focused, setFocused] = useState(false);
  const { current: clickRef } = useRef({ handleEvent });
  useEffect(() => {
    document.addEventListener("click", clickRef);
    return () => {
      document.removeEventListener("click", clickRef);
    };
  }, []);
  clickRef.current = element;
  clickRef.setFocused = setFocused;
  element.style.backgroundColor = focused ? "green" : "red";
});

function handleEvent({ target }) {
  const isFocused = this.current.contains(target);
  this.setFocused(isFocused);
}
@peerreynders
Copy link

peerreynders commented Oct 18, 2022

FYI: MDN used to document the EventListener interface but that page is now gone.

handleEvent() is only mentioned in passing in EventTarget.addEventListener()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment