DEV Community

Michael Warren
Michael Warren

Posted on

Preventable events: statelessness in stateful components

One of the biggest debates about authoring web components that I've had, both in my own mind and with coworkers is the debate over stateful vs stateless components. Is it better to have a component that manages a bit of its own state so that developers don't have to in their applications, or is it better that components manage no internal state and only use properties provided from the outside application to render.

There are pros and cons to either side of the question.

Pros & Cons of Stateless components

Easier to build
With the exception of form elements, completely stateless components are super easy to build. Each property has a certain set of allowed values and the component only re-renders when a property is changed, and only uses the outside properties to change what is rendered. Every functionality is exposed via the external API so that the outside world can manipulate it.

Native form inputs are a little harder to make stateless, because native HTML form inputs automatically have and track their value and validity states. Making an input behave as if it were stateless when the native element isn't purely stateless is very tricky.

Application state is the only state
Since stateless components don't hold any state, the application's state where components are used is the ONLY state. That way, there's never a chance of conflicting state where the component's internal state might be different than the application's state.

Flexible implementation for developers
Developers that use stateless components have full freedom to do what they need, when they need to, and they know that the component won't be trying to perform any logic or hold onto any internal state that might potentially conflict with the outside application state. Take the closing of a modal window for example:

<!-- This modal is closed because its `open` boolean attribute isn't present, and it won't open until the `open` attribute is added -->
<x-modal></x-modal>

<!-- This modal is open because its "open" boolean attribute is present, but it won't close until the `open` attribute is removed programmatically -->
<x-modal open></x-modal>
Enter fullscreen mode Exit fullscreen mode

With a completely stateless modal window, the developer gets to decide when the modal closes. If they need to do some extra functionality between the user deciding to close the modal and the modal actually closing, the freedom to do that is built in to the implementation strategy of the component.

Cons

Developers MUST recreate component state in their application state
Imagine a stateless component with a lot of available properties, and imagine a flow where lots of those properties need to be manipulated. Stateless components means that the application's state needs to be created/bound to component properties to manipulate the component in the desired ways. It's essentially a mirror of state that the component could have, or in some cases, already "does" have internally. It's also more lines of code in the application itself. It can be argued that components are created to encapsulate functionality and that internal state is part of it.

<!-- someBooleanVariable is application state that basically mirrors `xModal.open` -->
<x-modal open="${someBooleanVariable}"></x-modal>
Enter fullscreen mode Exit fullscreen mode

The more properties you need to manipulate in a given UI, the more closely to mirroring the component's state you'll actually be:

<!-- 'someObject' is basically a shallow clone of xComponent -->
<x-component
   active="${someObject.active}"
   status="${someObject.status}"
   variant="${someObject.variant}"
   label="${someObject.label}"
></x-component>
Enter fullscreen mode Exit fullscreen mode

And it gets worse if you are looping through repeated instances of the same component, like looping through rows in a table and managing each one's properties individually. In that case, your application state would be some array of objects, each one basically being a shallow copy of the component whose state you're managing.

Potential loss of consistency in component behavior
If each individual developer is completely in control of what each stateless component does, then you stand to risk some loss of consistency in component behavior. If you're making a design system whose main responsibility is consistency in user experience, statelessness might be a hindrance, depending on the component.

Take a stateless input for example, where it only displays an error state when the error parameter has a value.

<x-form-field error="Some error message"></x-form-field>
Enter fullscreen mode Exit fullscreen mode

Now envision that your organization has collectively made the rule that error messages should never be shown to users while they are typing but only after the form field has lost focus (yelling at users to fix an error they are currently trying to fix is bad form ). Our stateless form field above allows developers to show error messages at any time, even while typing. Preventing that behavior in order to preserve the desired user experience goes against the statelessness concept, because the component is doing something it wasn't told to do from the outside, ie - something like "when this form field is focused, never show error messages, regardless of what the error property is set to.

Can we have both?

Is it possible to have a component be mostly stateful to prevent application developers from needing to essentially clone our components in their application state and also to help keep consistent UX behaviors, but still selectively allow for them to prevent certain stateful behaviors when they need to?

Preventable events pattern

Event listeners is one of the main ways that component developers can respond to actions that happen within the boundaries of a web component. When a user clicks something, selects an option, checks a checkbox, chances are, some event is emitted to the outside application that lets that application know what happened, etc.

I'm sure that lots of folks reading this are probably already familiar with event.preventDefault() as we've previously used it to do things like prevent the default click event on links or buttons so that we can execute some JS before changing pages, but we can actually harness this function to enable components to be both stateful and stateless when we need them to be.

Since event listeners are all executed synchronously — that is, every event handler that is established on some DOM element is executed in a synchronous chain (outside in) before our JS code moves on — it is possible to check to see if a particular event was prevented and use that conditional to decide what to do next. In our case, we would check to see if the event was prevented and if so, NOT perform stateful property setting internally.

Let's look at our modal window example from before but make it a stateful modal window this time. Meaning, that when the user clicks the X button to close the modal, the modal window will close itself without the dev having to manually set the open property to false;

// xModal.js

class XModal extends LitElement {

  private internalModalClose() {
    // modal will close itself when the close button is clicked.
    this.open = false;
  }

  render() {
    return html`
       ...other modal stuff

       <button class="close-modal" @click="internalModalClose()">Close X</button>
    `;
  }

}
Enter fullscreen mode Exit fullscreen mode

This stateful-only approach saves one line of code in the outer application (for every modal instance), but if the developer needs to run some JS between the user clicking the close button and the modal actually closing, there's no way for that to happen.

But if we change the internal close button click handler to adopt the preventable event pattern, we'll get what we need!

// xModal.js

class XModal extends LitElement {

  private internalModalClose(event) {
    // prevent the native click event from bubbling so we can emit our custom event
    event.preventDefault();

    // create and dispatch our custom event
    const closeEvent = new CustomEvent('close-button-clicked');
    this.dispatchEvent(closeEvent);    

    // this if block will only execute AFTER all event handlers for the closeEvent have been executed
    // so its safe to check here to see if the event has been defaultPrevented or not
    if(!closeEvent.defaultPrevented) {
      // perform our stateful activity ONLY if closeEvent hasn't been defaultPrevented.
      this.open = false;
    }
  }

  render() {
    return html`
       ...other modal stuff

       <button class="close-modal" @click="internalModalClose()">Close X</button>
    `;
  }

}
Enter fullscreen mode Exit fullscreen mode

then when our mostly stateful component gets used

<!-- some-page.html-->

<x-modal @close-button-clicked="handleModalClose()"></x-modal>
Enter fullscreen mode Exit fullscreen mode
// somePage.js

handleModalClose($event) {
  // now the modal won't close itself automatically
  $event.preventDefault();

  ...do some stuff

  // set the open prop to false to close the modal when ready
  xModal.open = false;
}
Enter fullscreen mode Exit fullscreen mode

With this approach, it enables a component to be stateful, but also allow certain "escape hatches" for developers to take control in a stateless way.

Even the conceptual idea of "preventing the default behavior" fits semantically. You the component developer are allowing your component consumers the ability to prevent the default stateful behavior in a predictable way.

Library function

If you find yourself constantly dispatching custom events that you want to all be preventable, this approach is easily turned into a library or helper function to create and dispatch a preventable event and automatically check to see if that event is defaultPrevented before executing a callback.

Here's an example of a generic preventable event factory function:

const defaultEventOptions = {
  bubbles: true,
  cancelable: true,
  composed: true,
  detail: {}
};

const eventEmitter = function (dispatchElement: HTMLElement) {
  return function(eventName: string, eventOptions: EventInit, callback: () => void) {
    const actualEventOptions = Object.assign({}, defaultEventOptions, eventOptions);
    const event = new CustomEvent(eventName, actualEventOptions);

    dispatchElement.dispatchEvent(event);
    if(!event.defaultPrevented) {
      // if the event isn't prevented, run the callback function with the dispatchElement as `this` so class references in the callback will work
      callback.call(dispatchElement);
    }
  };

};
Enter fullscreen mode Exit fullscreen mode

and here's how that library function would get used in a component:

// xModal.js

class XModal extends LitElement {

  emitPreventable = eventEmitter(this);

  private internalModalClose(event) {
    this.emitPreventable('close-modal-clicked', undefined, () => {
      // won't execute unless the event isn't defaultPrevented
      this.open = false;
    });
  }

  render() {
    return html`
       ...other modal stuff

       <button class="close-modal" @click="internalModalClose()">Close X</button>
    `;
  }

}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This approach isn't applicable everywhere. It will only help with event-based features, which mostly centers around user interaction, so I wouldn't advertise this approach as enabling a component to be fully stateful AND fully stateless at the same time. Its not even a 50/50 mix of the two. If you want to make stateful components and you use an event-based strategy, this approach will enable you to provide more flexibility, but not necessarily ultimate flexibility.

Discussion (0)