Jan Miksovsky’s BlogArchive AboutRSSJSONContact

2017

Our current best answer for styling reusable components: subclassing

Even though styling reusable components is a hard problem, the Elix project needs a solution if it’s to keep moving forward. The library’s goal is to provide general-purpose components that can be styled/themed to meet customers’ needs. As far as we’re aware, neither the web platform nor component frameworks give us the styling primitives we need. For now, Elix is tackling this styling challenge with subclassing.

Asking a component what it wants to update

To begin, our solution relies on the previously-discussed ReactiveMixin, to define components in a React-ish, functional-reactive style. That post includes a live demo of a canonical increment/decrement component created with ReactiveMixin. The source shows a render function that updates DOM whenever state changes.

Let’s add custom styling and behavior to that increment/decrement component. We’ll start by using an Elix mixin called ShadowTemplateMixin to populate the shadow with the same template we used before:

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

Now we’ll make use of a new Elix mixin called RenderUpdatesMixin that asks the component for a set of updates to apply during rendering. The mixin will then update the DOM as requested. The component supplies this updates object as a property, indicating the attributes, classes, styles, and other properties to update:

  get updates() {
    return {
      style: {
        color: this.state.value < 0 ? 'red' : null
      },
      $: {
        visibleValue: {
          textContent: this.state.value
        }
      }
    };
  }

The top level keys of the updates object will be applied to the component’s host element. Here, the style key says that the host element’s style.color should be updated to 'red' when the value is negative, and left unspecified otherwise. It’s not shown above, but a component can also specify keys for attributes and classes to modify those aspects of the host element. Any keys that aren’t special are treated as custom properties and set directly.

The $ section contains updates that should be applied to elements in the component’s shadow. When ShadowTemplateMixin sees a template element with an id like <span id="visibleValue"></span>, it defines a reference this.$.visibleValue to point to that span. Here, the updates object is asking to update that span’s textContent to the current number in this.state.value.

This updates getter is equivalent to the imperative:

  [symbols.render]() {
    this.style.color = this.state.value < 0 ? 'red' : null;
    this.$.visibleValue.textContent = this.state.value;
  }

A component interaction pipeline

The use of RenderUpdatesMixin and other Elix mixins lets us construct a pipeline of sorts inside the component:

  events → methods/properties → setState → render → updates → updated DOM

When the user clicks the “+” or “-” buttons:

  1. A click event fires, which…
  2. sets the value property via a public API, which…
  3. invokes setState to update this.state.value, which…
  4. invokes an internal symbols.render() method that…
  5. asks the component for the state-dependent updates it wants to make which…
  6. get applied to the DOM.

And, as a result, the user sees the visible value number go up or down.

The updates are applied via helper functions that make the underlying DOM API calls. There’s no virtual DOM diff’ing going on here, but the number of updates is generally small and targeted to the elements that are actually changing. For the time being, performance seems reasonable.

Declarative formats as a last resort

As an aside, I’ve come to generally shy away from declarative formats like the updates object above. People like the concise nature of a declarative format for UI structure or behavior, and such a format can have a place in systems devs are willing to learn.

I think that learning cost is steep, so in code I want other people to use or contribute to, I try to avoid introducing declarative formats. Doing so is tantamount to shouting, Whee! I’ve invented a new domain-specific language for you to learn! The syntax may be JavaScript, but the semantics are opaque — it’s really a tiny interpreted language. Though my concise declarative language may be easy for me to understand, it’s impossible for you to know what effect it will have unless and until you’re willing to learn my new language.

So I currently avoid declarative code unless it has some concrete advantages.

Styling and specializing via subclasses

That said, in this case defining updates as an object does offer a real advantage: the updates can be augmented by mixins and subclasses.

When we say we want to let customers style a reusable component, that’s another way of saying we want to let people take existing code and specialize it. A component is just a class, and a traditional means to specialize a class is to create a subclass. So let’s see how subclassing can work here.

Since the updates property sits on the prototype chain, it can be overridden by a mixin or subclass that wants to add or adjust updates for the current state. A mixin/subclass can invoke super to get the base updates, then modify as that base value as it sees fit. E.g., someone could create a custom version of the generic increment/decrement component above:

  class CustomIncrementDecrement extends IncrementDecrement {

    get updates() {

      const base = super.updates;
      const baseColor = base.style && base.style.color;
      // Pick a color if the base class didn't specify one.
      const color =  baseColor || (this.state.value > 0 ? 'dodgerblue' : null);

      // Merge updates on top of those defined by the base class. This lets us
      // preserve some of the base rendering, while adding our own styling and
      // some unique behavior.
      return merge(base, {
        style: {
          background: 'lightgray',
          color,
          'font-family': 'Helvetica, Arial, sans-serif',
          'font-weight': 'bold'
        }
      });
    }

  }

Here the component indicates that its host element style should be updated with custom colors and fonts. Rather than focusing on CSS rule precedence, the prototype chain determines what updates apply — last writer wins. If you customize my class by subclassing it, your subclass has the last say.

This code relies on a merge helper that generally does a shallow merge, but goes deeper when merging the special keys attributes, classes, style, or $. The merging allows the updates cooperatively constructed by the base class, any mixins, and any subclasses to be efficiently computed and applied.

Applying such state-dependent styling is tricky in CSS: all state would first need to get rendered to the DOM as attributes, then CSS rules would have to be conditional on the presence of those attributes. Overriding such CSS rules requires carefully matching their precedence, otherwise customizations might be overly general or overly specific.

It’s worth noting that mixins/subclasses can inspect the updates requested by the base class, and incorporate those values into their own calculations. In the sample above, the subclass provides a blue color for positive values, but leaves alone the red color the base class provides for negative values.

Updating shadow parts

The above code only customizes the host element, which we could do via CSS directly. What we’re really after is a way to customize shadow parts: elements inside the shadow tree. Our customized increment/decrement component can do that through the $ key described earlier:

  get updates() {

    const base = super.updates;
    const baseColor = base.style && base.style.color;
    const color =  baseColor || (this.state.value > 0 ? 'dodgerblue' : null);

    const buttonStyle = {
      background: '#444',
      border: 'none',
      'border-radius': 0
    };
    const decrementDisabled = this.state.value <= -5;
    const incrementDisabled = this.state.value >= 5;

    return merge(super.updates, {
      style: {
        background: 'lightgray',
        color,
        'font-family': 'Helvetica, Arial, sans-serif',
        'font-weight': 'bold'
      },
      $: {
        decrement: {
          attributes: {
            disabled: decrementDisabled
          },
          style: merge(buttonStyle, {
            color: decrementDisabled ? 'gray' : 'white'
          })
        },
        increment: {
          attributes: {
            disabled: incrementDisabled
          },
          style: merge(buttonStyle, {
            color: incrementDisabled ? 'gray' : 'white'
          })
        }
      }
    });
  }

Live demo

Above we style the buttons with some base styling. We can also modify attributes or other properties. Here we arrange for the buttons to only allow input values between -5 and 5. (For completeness, we can also impose the same input bounds on the value property exposed in the public API.) We apply conditional styling to show the buttons differently when they’re enabled or disabled.

Mixins that update light and shadow DOM

If you’re reluctant to create a class hierarchy, you can do what Elix does and factor most of your code into functional mixins. Mixins allow your code to be reused across classes, and permit a great deal of flexibility.

For example, I’ve previously described how components often need to update light DOM to support ARIA attributes. To address that scenario, we’ve factored out ARIA attribute handling for list-like components into a mixin called AriaListMixin. That mixin augments the component’s updates getter to apply attributes like role, aria-orientation, and aria-activedescendant.

Results

We’ve successfully applied this architecture to the current Elix component set. Using a declarative updates object makes the code very concise, which is good — but also makes the code opaque to outsiders, which is bad. The main win is that we now have a workable method for creating custom-themed versions of these general-purpose components. Significantly, the themed components are just custom elements that can be used by clients like any other web components.

If others come up with other ways to style general-purpose web components, we’d be very interested. In the meantime, we at least have a way to keep moving forward.

Styling is critical to web component reuse, but may prove difficult in practice

The easiest way to create web components with a distinctive visual style is to bake that style directly into the components’ code. Most user interface components are designed to be used solely within the company creating them, in which case baking in styles may be acceptable. But anyone aspiring to create or consume reusable general-purpose web components will have to grapple with the fact that styling components is currently an unsolved problem.

That’s worrisome. Most web components we’ve seen have a built-in visual style so distinctive that, without modification, it would look out of place in an app with someone else’s brand. Not being able to easily theme such a component limits its utility.

Suppose you’re writing a hypothetical custom element called reusable-component and want to let other people style that element. Let’s consider your options for doing so.

Use CSS Custom Properties

If you think someone wants to change the background color of your reusable-component, you can define its background-color style with a CSS Custom Property:

  :host {
    background-color: var(--reusable-component-background-color);
  }

and your users can then style your component by defining that custom property:

  :root {
    --reusable-component-background-color: blue;
  }

With the recent release of Edge 16, the above would now work in all modern browsers.

Unfortunately, this just doesn’t scale well. It’s a pain for you, who must define a new custom property for every CSS attribute someone might conceivably want to override. If your component has internal shadow parts — buttons, etc. — that your users might want to style, you have to define new custom properties for all the interesting CSS attibutes on all those parts. And this will be painful for your users, who have to learn a long new list of custom properties for every component they might want to style.

Wait for CSS parts and themes

To solve the above problem, there’s a proposal for CSS Shadow Parts which would make it possible to expose designated internal parts as pseudo-elements that can be styled from the outside. This is similar to the way you can style certain native HTML elements in a non-standard way with certain pseudo-elements. For example, WebKit exposes the thumb (handle) of a scroll bar as a pseudo-element ::-webkit-scrollbar-thumb, so you can write

  ::-webkit-scrollbar-thumb {
    background: pink;
  }

to make scroll bar thumbs pink.

The CSS Shadow Parts spec would let you expose your component’s internal parts for styling. If you had a commit button in the shadow of your reusable-component, you could expose it as a part:

  <button part="commit-button">...</button>

And someone can then write:

  reusable-component::part(commit-button) {
    background: pink;
    border: 1px solid red;
    border-radius: 5px;
  }

to style the button. That’s a more convenient way to achieve the same thing as CSS Custom Properties. There’s less to document for your users and more flexibility. Your users can apply whatever styling they want without you needing to anticipate everything they might want to do.

There are some downsides, though, when it comes to reusing such a styled custom element.

Wrapping reusable custom elements + styling

One key advantage to giving your customer a custom element is that they end up with a single thing that produces a consistent result. But if your customer tries to style your reusable-component, they’ll end up creating a new reuse problem for themselves. People at that company now have to deal with two separate things: 1) your original reusable-component definition, and 2) a stylesheeet with the company’s styling for reusable-component and its parts. On their own, those two entities are independent, with no explicit relationship. Having to track and apply them correctly creates maintenance headaches.

Your customer could define a new component-wrapper element that wraps your original reusable-component and applies the desired styling:

  <template>
    <style>
      reusable-component::part(commit-button) {
        background: pink;
        border: 1px solid red;
        border-radius: 5px;
      }
    </style>
    <reusable-component>
      <slot></slot>
    </reusable-component>
  </template>

Then your customer can distribute this component-wrapper component internally, and everyone gets both your internal reusable-component and the correct styling in a nice package. (Even if reusable-component is actually defined elsewhere, component-wrapper can express that dependency, so the pieces are linked together.)

But this introduces new challenges:

Overall, without an easy repackaging mechanism for themed components, organizations may have difficulty adopting and styling components they acquire from elsewhere.

Overriding styles can be complex

It’s been our experience that even general-purpose components can end up with complex styling. People tend to approach component styling/theming as if the components were completely static, but components have dynamic state. State is often implicated in styling. As an example, a native button that’s disabled shows different styling than an enabled button. If someone isn’t carefully considering the :disabled pseudo-class in their button styling, they may end up applying an enabled button appearance to a disabled button.

Web components can have complex internal states, resulting in correspondingly complex internal stylesheets. Overriding such styles will be a delicate matter. To look at some concrete examples, I looked through some internal stylesheets in web components we’ve previously written. Here are some of the CSS selectors I found:

  // From a carousel component
  :host(.overlayArrows) .navigationButton:hover:not(:disabled) { ... }

  // From a tabs component
  :host([generic=""][tab-position="right"]) .tab:not(:last-child) { ... }

  // From a toast component
  :host([from-edge="bottom-right"].opened:not(.effect)) #overlayContent,
  :host([from-edge="bottom-right"].effect.opening) #overlayContent { ... }

Each complex CSS selector in your component may create a challenge for your customer. If the tabs component above exposes an individual tab as a part, what about that :not(:last-child) bit? If your customer writes styles that target the tab part, what styling should apply to the last tab?

In general, even if you can expose an interesting internal part of the component for outside styling, your customer will need to be aware of a large number of conditions that may apply. They could easily end up writing rules that don’t apply (because more specific conditions exist that take precedence) or apply when they shouldn’t (they write rules that are too general).

This is not to say that the CSS Shadow Parts spec won’t be a step forward — it will be — but rather to say that styling components with of normal complexity might turn out to be extremely challenging in practice.

(Aside: It goes without saying that, even if the browser vendors are excited about CSS Shadow Parts and the spec speeds through the standards process, it could still be a very long time until you can take advantage of them in all the browsers and devices you care about. And for what it’s worth, polyfilling new CSS syntax is notoriously difficult to do well.)

Inline styling

Your customer trying to use your reusable-component might accomplish a certain degree of styling by applying inline styles to the component’s host element (the top-level <reusable-component> instance sitting in the light DOM). Users of React and other FRP frameworks have found inline styling a powerful way to have a component apply styles to subelements. And more generally, inline styling is usually the easiest way to programmatically adjust an element’s appearance regardless of framework.

However, there are serious challenges using inline styles with web components. Inline styles can’t be used to style internal component parts. That will remain true even if and when the CSS Shadow Parts proposal is adopted, as that only addresses styling with stylesheets. And as noted above, components can have complex state. Writing inline styles for the host element that apply in all conditions is likely too blunt an instrument.

React and similar frameworks already struggle somewhat to deal with styling, but an increase in the presence of complex general-purpose components will make the issue more pressing.

Other options?

Given the above, we’re not sure that either CSS Custom Properties, CSS Shadow Parts, or inline styling will be sufficent. We think those platform features are really interesting — it’s just that they may not be enough for what we want to do. We want to create reusable web components that companies can easily brand for their applications, and we’re unsure how to deliver that.

We’re exploring alternative ideas for letting people style our web components, but are very interested in hearing how other people are tackling this problem. If you have ideas, please share.

Is it worth creating web components that work on IE 11? Or Edge?

I spent the last week on my least favorite engineering task: trying to get a body of code that works on Chrome/Safari/Firefox to work on Microsoft Edge and Microsoft Internet Explorer. In this case, I’ve been trying to get the Elix project’s unit tests and basic component set working as expected in Edge and IE 11. Such work is never fun. Lately I’ve been wondering whether it’s worth the Elix project’s time to support Microsoft’s browsers.

Internet Explorer 11

IE 11 is still supported by Microsoft, but as the mainstream browsers have accelerated away from it, working in IE feels increasingly anachronistic. Although many modern web technologies come with a polyfill or other means to accommodate IE 11, the set of workarounds required today has really piled up.

In the case of the Elix project, here’s the current set of things we need to do for IE 11:

Everything we’re forced to add to the above list moves us further and further away from the metal. When we hit a bug, it’s really hard to be confident about where the bug lies. Is it our code? Or somewhere in the list above?

I joked on Twitter that getting a modern web app to run on IE is possible, in the same way it’s possible to play Doom on a thermostat. In truth, the situation is worse. For all I know, a modern thermostat has better hardware than the 1993 PCs which Doom originally ran on. A better comparison might be that getting a modern web app to run on IE is like getting a modern game title like Horizon Zero Dawn to run on a thermostat.

Edge

Microsoft Edge 16 is much better than IE, but it’s still no picnic.

While Edge supports many modern web technologies, Microsoft still hasn’t begun implementing Shadow DOM and Custom Elements. And Edge still suffers from some of the same painful, glaring problems as its predecessor:

It’s not that Microsoft has forgotten about developers and how to cater to them. I’m continually impressed by the speed and quality of the work currently going into Visual Studio Code and TypeScript, for example. But when it comes to the developer experience in modern browsers, Edge is dead last.

Market share and inertia

In discussions about browser support, IE and Edge support are often presumed to be important. But given the current state of the market and some usage summaries, I’m not sure that makes sense.

Mobile browser usage exceeds desktop browser usage. On mobile, China’s UC Browser appears to have significantly more market share than IE and Edge on the desktop. Samsung Internet for Android likewise may have already passed Edge in market share, and may soon pass IE.

The cost to keep things working on IE steadily grows. Even when new web advances come with polyfills that run on IE, the combined weight of all that’s necessary to support IE is considerable. How much faster could your team go if it didn’t have to support IE? In the case of the Elix project, I’m guessing IE support soaks up 10% of our time, and 50% of our positive emotions.

And though Edge is Microsoft’s replacement for IE, it’s not clear Edge is on a path to any kind of interesting market position. In the 2 years Edge has been on the market, it’s made miniscule gains. As far as I can tell, everyone who can abandon IE has already moved to Chrome. And those Chrome users must be sticking with Chrome even when they upgrade to a Windows machine capable of running Edge.

In the global political order, the country of France retains one of 5 permanent seats on the U.N. Security Council solely for historical reasons, out of all proportion to its current importance. It feels like similar historical reasons may soon be the primary justification for Microsoft’s position on web app browser requirements lists. Microsoft acts as if it automatically deserves a seat at the table, but I question that. It’s reasonable to ask Microsoft: What are you doing, today, as a browser vendor, that makes you worth the time and energy you force developers to spend on you?

As someone who worked at Microsoft for many years, I hold no grudge against them. To the contrary, as an alum, I really want them to be successful. But if they’re going to be relevant as a browser vendor, they’re going to have to do a lot better. In the meantime, I’m wondering whether their browsers are worth the trouble.

Your web components with Shadow DOM may need to update light DOM too

Web components and Shadow DOM are practically synonymous, but even web components with a shadow subtree often need to render information into the light DOM. A component might need to:

Example: ARIA support

Suppose you’re creating a single-selection list component, and want to follow the ARIA best practices for list boxes. Perhaps you use your favorite web component library to create a shadow root for your component and clone a template into it. Your component’s shadow might include, among other things, styling for your list container and its light DOM items:

  <template>
    <style>
      :host {
        /* Host element styling goes here */
      }

      ::slotted(*) {
        /* General list item styling goes here */
      }

      ::slotted([aria-selected="true"]) {
        /* Styling for the selected list item goes here */
      }
    </style>
    <slot></slot>
  </template>

Maybe that’s all that’s happening on the Shadow DOM side of things. Your component will also need to do the following work in the light DOM:

  1. Set role="listbox" on the host element.
  2. If the list is horizontal, set aria-orientation="horizontal".
  3. Set role="option" on all items in the list. Be careful not to mark any auxiliary content like style as items in the list!
  4. Set aria-selected="true" on the selected item. (For what it’s worth: I’ve encountered at least one web component bug where the NDVA screen reader required aria-selected="false" to be set on all other elements, even for a single selection list.)
  5. Set the host’s aria-activedescendant attribute to be the id of the currently-selected item. If the page author hasn’t supplied id attributes for every item, you will need to generate and assign an id for those items.

That’s a lot of work going on in the light DOM! These updates to the light DOM may surprise a page author if they include your list component in markup:

  <accessible-list aria-label="Fruits" tabindex="0">
    <div>Apple</div>
    <div>Banana</div>
    <div>Cherry</div>
  </accessible-list>

At runtime, when this component updates the light DOM, the result might be:

  <accessible-list aria-label="Fruits" tabindex="0" role="listbox"
      aria-activedescendant="_option0">
    <div role="option" id="_option0" aria-selected="true">
      Apple
    </div>
    <div role="option" id="_option1" aria-selected="false">
      Banana
    </div>
    <div role="option" id="_option2" aria-selected="false">
      Cherry
    </div>
  </accessible-list>

All this ARIA work is happening in the light DOM, not the Shadow DOM. Work is underway on a better accessibility API, but ARIA attributes are the only solution for the foreseeable future. And as outlined above, your component might have other reasons to update the light DOM.

Generally speaking, you’ll need to write the code to update the light DOM yourself. Most web component frameworks to date have focused on updating Shadow DOM, not light DOM.

Conclusion

What goes on in a component’s shadow may only be half the picture — a substantial amount of work may be going on in the light DOM. That’s an important point to consider when you’re deciding how you want to write your component. Most component frameworks are focused on rendering Shadow DOM, so you’ll need to understand what light DOM updates are appropriate and make them yourself.

Code to handle such cases can be complex. For that reason, the Elix project tries to identify common scenarios for updating light DOM and address those with mixins like AriaListMixin.

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:


&lt;template id="template">
  &lt;button id="decrement">-&lt;/button>
  &lt;span id="value">&lt;/span>
  &lt;button id="increment">+&lt;/button>
&lt;/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.