Eventing Like React

03 Sep 2024 - Ben

I have a previous article about using React to mirror DOM eventing style while still being able to use other React goodies. In that article, I gave an example of Submitting or Skipping a form. You might think that sort of semantic is something only React can do, but that’s far from the truth! With Custom Events we can do the exact same thing with a normal form element. The code looks much different, but the end result is the same. Or maybe even more powerful, given it uses the browser to it’s full advantage, instead of glossing over it like React does.

The browser has so many rich APIs for interacting, including a strong eventing system! Using it, we get so much for free and can easily listen to the parts of the document we need. That’s a blessing and a curse, if we aren’t careful we could end up with a convoluted mess. I’ve managed to make convoluted messes in React too, though. This skip example is a great example of how powerful augmenting it directly can be. Let’s dive in:

<body>
  <form>
    <input name="name">
    <button type="submit">Submit</button>
    <!-- inline is not recommended, but used for illustrative purposes -->
    <button type="button" onclick="skipForm">Skip</button>
  </form>
  <script>
    /**
     * A barebones implementation of our skip event
     * @example
     * elem.dispatchEvent(new SkipEvent());
     * */
    class SkipEvent extends CustomEvent {
        constructor(detail) {
            super('skip', {
              detail,
              bubbles: true,
              cancelable: true
            });
        }
    }

    /**
     * fires a SkipEvent
     * */
    function skipForm(event) {
      event.stopPropagation();
      this.dispatchEvent(new SkipEvent())
    }

    // add listeners on the document so we can see it fire when it bubbles up
    document.addEventListener('skip', (e) => console.log('form skipped', e));
  </script>
</body>

Which will show up on our document as a skipped event! This is the parallel to how our onSkip handler worked in React, except this version pulls in all of the power of the browser’s native event system! We can attach that behavior to any element we want very easily because we’ve described the behavior and the view separately!

One Step Further

In our above example, we’ve only fired our skip event on the button. We could easily fire it on the form as well, maybe if we receive a blank submission. Unlike our React example, we can actually get better granularity here! In React, we can’t tell what caused onSkip to be called. The skip button might have fired the event, or an empty input may have! Since DOM Events have a target property, it’s easy to check. We can model this in React by passing the SynctheticEvent as part of our onSkip callback, but the DOM provides this out of the box.

<body>
  <form>
    <!-- inline is not recommended, but used for illustrative purposes -->
    <input name="name" onsubmit="submitForm">
    <button type="submit">Submit</button>
    <button type="button" onclick="skipForm">Skip</button>
  </form>
  <script>
    /**
     * A barebones implementation of our skip event
     * @example
     * elem.dispatchEvent(new SkipEvent());
     * */
    class SkipEvent extends CustomEvent {
        constructor(detail) {
            super('skip', {
              detail,
              bubbles: true,
              cancelable: true
            });
        }
    }

    /**
     * fires a SkipEvent
     * */
    function skipForm(event) {
      event.stopPropagation();
      this.dispatchEvent(new SkipEvent())
    }

    /**
     * skips if empty input, submits if not
     */
    function submitForm(event) {
      event.preventDefault(); // so the page doesn't refresh on us
      const formData = new FormData(event.target); // the data the form is submitting
      // get the name out of our form data, if it's blank stop the current event and fire a SkipEvent from here
      const name = formData.get('name');
      if(!name) {
        event.stopPropagation();
        this.dispatchEvent(new SkipEvent());
      }
    }

    // add listeners on the document so we can see it fire when it bubbles up
    document.addEventListener('skip', (e) => console.log('form skipped', e));
    document.addEventListener('submit', (e) => console.log('form submitted', e));
  </script>
</body>


Which version do you like better? Which is more legible? Which do you think is easier to maintain? Which provides more context? Which one can communicate with the backend more easily?

My last parting thought is that the above is a complete body to a webpage. Give it a head and doctype and you can host it anywhere. Just use it like that and a browser is probably kind enough to show it to you still. In my React example, there’s some boilerplate to bootstrap the app client side I didn’t include. Why not just use the native client side, instead of requiring a bootstrapped context?