A compact JavaScript mixin for creating native web components in FRP/React style

Perhaps you like the benefits of functional reactive programming, but would like to create native web components with minimal overhead. This post explores a relatively simple JavaScript mixin that lets you author web components in a functional reactive programming (FRP) style modeled after React. This mixin focuses exclusively on managing state and determining when the state should be rendered. You can use this mixin with whatever DOM rendering technology you like: virtual-dom, hyperHTML, lit-html, plain old DOM API calls, etc.

I spent several months this summer writing React and Preact components, and the values of an FRP model were immediately clear. As React advocates claim, using FRP does indeed make state easier to reason about and debug, code cleaner, and tests easier to write.

I wanted to bring these reactive benefits to the Elix web components project which Component Kitchen leads. Elix already uses functional mixins extensively for all aspects of component functionality for everything from accessibility to touch gestures. I wanted to see if it were possible to isolate the core of an FRP architecture into a functional mixin that could be applied directly to HTMLElement to create a reactive web component.

You can use the resulting ReactiveMixin to create native web components in a functional reactive style. FRP frameworks often use a canonical increment/decrement component as an example. The ReactiveMixin version looks like this:

  
    import ReactiveMixin from '.../ReactiveMixin.js';

    // Create a native web component with reactive behavior.
    class IncrementDecrement extends ReactiveMixin(HTMLElement) {

      // This property becomes the initial value of this.state at constructor time.
      get defaultState() {
        return { value: 0 };
      }

      // Provide a public property that gets/sets state.
      get value() {
        return this.state.value;
      }
      set value(value) {
        this.setState({ value });
      }

      // Expose "value" as an attribute.
      attributeChangedCallback(attributeName, oldValue, newValue) {
        if (attributeName === 'value') {
          this.value = parseInt(newValue);
        }
      }
      static get observedAttributes() {
        return ['value'];
      }

      // … Plus rendering code, with several options for rendering engine
    }

    customElements.define('increment-decrement', IncrementDecrement);

Live demo

You end up with something that’s very similar to React’s Component class (or, more specifically, PureComponent), but is a native HTML web component. The compact mixin provides a small core of features that enable reactive web component development in a flexible way.

Defining state

ReactiveMixin gives the component a member called this.state, a dictionary object with all state defined by the component and any of its other mixins. The state member, which is read-only and immutable, can be referenced during rendering, and to provide backing for public properties like the value getter above.

ReactiveMixin provides a setState method the component invokes to update its own state. The mixin sets the initial state in the constructor by passing the value of the defaultState property to setState.

Detecting state changes

When you call setState, ReactiveMixin updates the component’s state, and then invokes a shouldComponentUpdate method to determine whether the component should be rerendered.

The default implementation of shouldComponentUpdate method performs a shallow check on the state properties: if any top-level state properties have changed identity or value, the component is considered dirty, prompting a rerender. This is comparable to the similar behavior in React.PureComponent. In our explorations, we have found that our web components tend to have shallow state, so pure components are a natural fit. You can override this to provide a looser dirty check (like React.Component) or a tighter one (to optimize performance, or handle components with deep state objects).

If there are changes and the component is in the DOM, the new state will be rendered.

Rendering

This mixin stays intentionally independent of the way you want to render state to the DOM. Instead, the mixin invokes an internal component method whenever your component should render, and that method can invoke whatever DOM updating technique you like. This could be a virtual DOM engine, or you could just do it with plain DOM API calls.

Here’s a plain DOM API render implementation for the increment/decrement example above. We’ll start with a template:


    <template id="template">
      <button id="decrement">-</button>
      <span id="value"></span>
      <button id="increment">+</button>
    </template>

To the component code above, we’ll add an internal render method for ReactiveMixin to invoke. The mixin uses a Symbol object to identify the internal render method. This avoids name collisions, and discourages someone from trying to invoke the render method from the outside. (The render method can become a private method when JavaScript supports those.)


    import symbols from ‘.../symbols.js’;

    // This goes in the IncrementDecrement class ...
    [symbols.render]() {
      if (!this.shadowRoot) {
        // On our first render, clone the template into a shadow root.
        const root = this.attachShadow({ mode: 'open' });
        const clone = document.importNode(template.content, true);
        root.appendChild(clone);
        // Wire up event handlers too.
        root.querySelector('#decrement').addEventListener('click', () => {
          this.value--;
        });
        root.querySelector('#increment').addEventListener('click', () => {
          this.value++;
        });
      }
      // Render the state into the shadow.
      this.shadowRoot.querySelector('#value').textContent = this.state.value;
    }

That’s all that’s necessary. The last line is the core bit that will update the DOM every time the state changes. The two buttons update state by setting the value property, which in turn calls setState.

This ReactiveMixin would also be a natural fit with template literal libraries like lit-html or hyperHTML. That could look like:


    import { html, render } from ‘.../lit-html.js’;
    import symbols from ‘.../symbols.js’;

    // Render using an HTML template literal.
    [symbols.render]() {
      if (!this.shadowRoot) {
        this.attachShadow({ mode: 'open' });
      }
      const template = html`
        <button on-click=${() => this.value-- }>-</button>
        <span>${this.state.value}</span>
        <button on-click=${() => this.value++ }>+</button>
      `;
      render(template, this.shadowRoot);
    }
  

The creation of the shadow root and the invocation of the rendering engine are boilerplate you could factor into a separate mixin to complement ReactiveMixin.

Web component and FRP lifecycle methods

Since components created with this mixin are still regular web components, they receive all the standard lifecycle methods. ReactiveMixin augments connectedCallback so that a component will be rendered when it’s first added to the DOM.

The mixin provides React-style lifecycle methods for componentDidMount (invoked when the component has finished rendering for the first time) and componentDidUpdate (whenever the component has completed a subsequent rerender). The mixin doesn’t provide componentWillUnmount; use the standard disconnectedCallback instead. Similarly, use the standard attributeChangedCallback instead of componentWillReceiveProps.

Conclusion

This ReactiveMixin gives us much of what we like about React, but lets us write web components which are closer to the metal. All it does is help us manage a component’s state, then tell our component when it needs to render that state to the DOM. Separating state management from rendering is useful — we’ve already changed our minds about which rendering engine to use several times, but those changes entailed only minimal updates to our components.

The coding experience feels similar to React’s, although I don’t see a need to make the experience identical. For example, I thought setState would work well as an `async` Promise-returning function so that you can wait for a new state to be applied. And it’s nice to avoid all the platform-obscuring abstractions (e.g., synthetic events) React pushes on you.

We’re using this ReactiveMixin to rewrite the Elix components in a functional reactive style. That work is proceeding fairly smoothly, and we’re moving towards an initial 1.0 release of Elix that uses this approach in the near future. In the meantime, if you’d like to play with using this mixin to create web components, give it a try and let us know how it goes.

Tweet

« Blog home