ReactiveMixin

Manages component state and renders changes in state

Overview

Purpose: Give a component class a functional-reactive programming (FRP) architecture that can track internal state and render that state to the DOM.

This mixin forms a core part of the Elix render pipeline, managing a component's state and rendering the component when state changes.

events → methods → setStaterender DOM → post-render

Expects the component to provide:

  • internal.render method that actually updates the DOM. You can use ShadowTemplateMixin to help populate the component's initial Shadow DOM tree. Beyond that, you will need to write a internal.render method to update elements in response to state changes; see Rendering below.
  • internal.rendered method that runs after the component renders.
  • internal.firstRender property that is undefined before the first render, true during the first render and rendered calls, and false in subsequent renders.

Provides the component with:

  • internal.state property representing the current state.
  • internal.setState() method to change state.

ReactiveMixin represents a minimal implementation of the functional-reactive programming architecture populate in React and similar frameworks. The mixin itself 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: hyperHTML, lit-html, virtual-dom, etc., or plain old DOM API calls like Elix does (see below).

The Elix project itself uses ReactiveMixin as a core part of all its components, so the mixin is included in the ReactiveElement base class. Elix components generally use the ReactiveElement base class instead of using ReactiveMixin directly.

Example

Functional-reactive frameworks often use a canonical increment/decrement component as an example. The ReactiveMixin version looks like this:

import * as internal from "elix/src/internal.js";
import ReactiveMixin from "elix/src/core/ReactiveMixin.js";

// Create a native web component with reactive behavior.
class IncrementDecrement extends ReactiveMixin(HTMLElement) {
  // This property becomes the initial value of this[internal.state] at constructor time.
  get [internal.defaultState]() {
    return { value: 0 };
  }

  // Provide a public property that gets/sets state.
  get value() {
    return this[internal.state].value;
  }
  set value(value) {
    this[internal.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);
Demo: A simple increment/decrement component defined with ReactiveMixin

ReactiveMixin provides a foundation very similar to React’s Component class (or, more specifically, PureComponent), but for native HTML web components. 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 property called state, a dictionary object with all state defined by the component and any of its other mixins. The state property itself is read-only and immutable. You can reference it 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. You can invoke setState in response to user interaction. (How you wire up event handlers is up to you; the Rendering section below explores some ways to handle events.)

Ensuring state consistency

While state members should generally be independent of each other, sometimes two or more state members have some interrelationship. If such state members are managed by multiple mixins, it is necessary to provide some means for the mixins to verify that a new state is consistent with their expectations.

For example, SingleSelectionMixin manages a state member called selectedIndex, and the independent ContentItemsMixin manages a separate but related state member called items. We would like to guarantee that the selectedIndex value should always be a valid array index into the items array (or the special value -1, which indicates no selection).

To maintain this invariant, if ContentItemsMixin updates the items array, SingleSelectionMixin needs to check that selectedIndex is still a valid array index. If not, it will want to adjust selectedIndex to fall within the new bounds of the items array. Exactly how it will do that depends on several factors, but a simplistic answer is that, if selectedIndex is now too large, it will be clipped to the new size of the array.

SingleSelectionMixin accomplishes this by defining a function called stateEffects. When setState is called to change state, ReactiveMixin will invoke stateEffects to see whether any mixin or class believes those state changes should cause any second-order effects. A much-simplified version of the code looks like this:

function SingleSelectionMixin(Base) {
  return class SingleSelection extends Base {
    get [internal.stateEffects](state, changed) {
      // See if base classes define second-order effects of their own.
      const effects = super[internal.stateEffects]
        ? super[internal.stateEffects](state, changed)
        : {};
      if (changed.items) {
        // Ensure index within bounds of -1 .. length-1.
        const { items, selectedIndex } = state;
        const length = items.length;
        const boundedIndex = Math.max(Math.min(selectedIndex, length - 1), -1);
        // New index is a second-order effect of the items change.
        Object.assign(effects, {
          selectedIndex: boundedIndex
        });
      }
      return effects;
    }
  };
}

When ContentItemsMixin invokes setState to make a change to the items property, ReactiveMixin will invoke the element's stateEffects method, including the method implementation shown here. The changed parameter will include the flag changed.items with a value of true to let the above code know that state.items have changed. In that case, the above code will then calculate a new selectedIndex that falls within the new bounds of the new items array.

The method here returns that new selectedIndex as a second-order effect of the change in state. The new selectedIndex value may or may not actually be different than the current selectedIndex. If it is different, then ReactiveMixin will apply the effects to the state and invoke stateEffects a second time.

On this second invocation of stateEffects, changed.selectedIndex will be true, but changed.items will no longer be true. Hence, the code above will make no further modifications to state.

In this way, stateEffects are computed and applied repeatedly. Eventually stateEffects will return an empty object, indicating that all mixins and classes believe the state is now consistent. The new, consistent state becomes the element's new official state.

Naturally, it's possible to create situations in which multiple mixins or classes repeated affect state in such a way as to trigger an infinite loop. However, the Elix project has found that, at the scale of Elix's web components, such situations can easily be avoided with proper care. State changes typically converge quickly to a stable state.

Rendering

When you call internal.setState, ReactiveMixin updates your component’s state. It also checks to see whether any state members actually changed, using a shallow comparison by value for this purpose. If state members actually changed and the component is in the DOM, the component is considered dirty, and will prompt a render call.

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 an identifier from the internal module to identify the internal render method. This avoids name collisions, and discourages someone from trying to invoke the render method from the outside.

import { ReactiveMixin, internal } from "elix";

class IncrementDecrement extends ReactiveMixin(HTMLElement) {
  // The following would be added to the earlier component definition...

  [internal.render](changed) {
    super[internal.render](changed);

    // On our first render, clone the template into a shadow root.
    if (this[internal.firstRender]) {
      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++;
      });
    }

    // When the value changes, show the value as the span's text.
    if (changed.value) {
      const value = this[internal.state].value;
      this.shadowRoot.querySelector("#value").textContent = value;
    }
  }
}

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.

The above example can be simplified by taking advantage of HTML templates. One way to do that is with ShadowTemplateMixin, which is included in the ReactiveElement base class. See that class for an example.

Lifecycle methods

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

When you invoke setState, ReactiveMixin will:

  1. Asynchronously invoke your component's internal.render method, passing in a set of changed flags indicating which state fields have actually changed. During the render call, you must avoid calling setState.
  2. Invoke your component's internal.rendered method when the above render call has completed, again passing in a set of changed flags. This is an opportune place for you to perform work that requires the component to be fully rendered, such as setting focus on a shadow element or inspecting the computed style of an element. If such work should result in a change in component state, you can safely call setState during the rendered method.

The first time your component is rendered, your component's internal.firstRender property will be true. You can use that flag in internal.render or internal.rendered to perform work that should only be done once, such as wiring up event listeners to elements in your component's shadow tree.

Note: Elix 11.0 and earlier defined internal.componentDidMount and internal.componentDidUpdate methods which have been deprecated. Use the internal.rendered method instead, and inspect the value of internal.firstRender. If that property is true, you can perform the work you previously did in internal.componentDidMount; if false, you can perform the work you previously did in internal.componentDidUpdate. If you are wiring up event handlers, you can likely perform that work earlier in the lifecycle during internal.render, rather than waiting for internal.rendered.

API

Used by classes AlertDialog, AutoCompleteComboBox, AutoCompleteInput, AutoSizeTextarea, Backdrop, Button, CalendarDay, CalendarDayButton, CalendarDayNamesHeader, CalendarDays, CalendarMonth, CalendarMonthNavigator, CalendarMonthYearHeader, Carousel, CarouselSlideshow, CarouselWithThumbnails, CenteredStrip, ComboBox, CrossfadeStage, DateComboBox, DateInput, Dialog, Drawer, DrawerWithGrip, DropdownList, ExpandablePanel, ExpandableSection, Explorer, FilterComboBox, FilterListBox, HamburgerMenuButton, Hidden, Input, ListBox, ListComboBox, ListExplorer, ListWithSearch, Menu, MenuButton, MenuItem, MenuSeparator, ModalBackdrop, Modes, NumberSpinBox, Overlay, OverlayFrame, PlainAlertDialog, PlainArrowDirectionButton, PlainAutoCompleteComboBox, PlainAutoCompleteInput, PlainAutoSizeTextarea, PlainBackdrop, PlainBorderButton, PlainButton, PlainCalendarDay, PlainCalendarDayButton, PlainCalendarDayNamesHeader, PlainCalendarDays, PlainCalendarMonth, PlainCalendarMonthNavigator, PlainCalendarMonthYearHeader, PlainCarousel, PlainCarouselSlideshow, PlainCarouselWithThumbnails, PlainCenteredStrip, PlainCenteredStripHighlight, PlainCenteredStripOpacity, PlainComboBox, PlainCrossfadeStage, PlainDateComboBox, PlainDateInput, PlainDialog, PlainDrawer, PlainDrawerWithGrip, PlainDropdownList, PlainExpandablePanel, PlainExpandableSection, PlainExplorer, PlainFilterComboBox, PlainFilterListBox, PlainHamburgerMenuButton, PlainHidden, PlainInput, PlainListBox, PlainListComboBox, PlainListExplorer, PlainListWithSearch, PlainMenu, PlainMenuButton, PlainMenuItem, PlainMenuSeparator, PlainModalBackdrop, PlainModes, PlainNumberSpinBox, PlainOverlay, PlainOverlayFrame, PlainPageDot, PlainPopup, PlainPopupButton, PlainPopupSource, PlainProgressSpinner, PlainPullToRefresh, PlainRepeatButton, PlainSelectableButton, PlainSlideshow, PlainSlideshowWithPlayControls, PlainSlidingPages, PlainSlidingStage, PlainSpinBox, PlainTabButton, PlainTabs, PlainTabStrip, PlainToast, Popup, PopupButton, PopupSource, ProgressSpinner, PullToRefresh, ReactiveElement, RepeatButton, SelectableButton, Slideshow, SlideshowWithPlayControls, SlidingPages, SlidingStage, SpinBox, TabButton, Tabs, TabStrip, Toast, and WrappedStandardElement.

[internal.defaultState] property

The default state for the component. This can be extended by mixins and classes to provide additional default state.

Type: PlainObject

[internal.render](changed) method

Render the indicated changes in state to the DOM.

The default implementation of this method does nothing. Override this method in your component to update your component's host element and any shadow elements to reflect the component's new state. See the rendering example.

Be sure to call super in your method implementation so that your component's base classes and mixins have a chance to perform their own render work.

Parameters:

  • changed: ChangedFlagsdictionary of flags indicating which state members have changed since the last render

[internal.renderChanges]() method

Render any pending component changes to the DOM.

This method does nothing if the state has not changed since the last render call.

ReactiveMixin will invoke this method following a setState call; you should not need to invoke this method yourself.

This method invokes the internal render method, then invokes the rendered method.

[internal.rendered](changed) method

Perform any work that must happen after state changes have been rendered to the DOM.

The default implementation of this method does nothing. Override this method in your component to perform work that requires the component to be fully rendered, such as setting focus on a shadow element or inspecting the computed style of an element. If such work should result in a change in component state, you can safely call setState during the rendered method.

Be sure to call super in your method implementation so that your component's base classes and mixins have a chance to perform their own post-render work.

Parameters:

  • changed: ChangedFlags

[internal.setState](changes) method

Update the component's state by merging the specified changes on top of the existing state. If the component is connected to the document, and the new state has changed, this returns a promise to asynchronously render the component. Otherwise, this returns a resolved promise.

Parameters:

  • changes: PlainObjectthe changes to apply to the element's state

Returns: Promise - resolves when the new state has been rendered

[internal.state] property

The component's current state.

The returned state object is immutable. To update it, invoke internal.setState.

It's extremely useful to be able to inspect component state while debugging. If you append ?elixdebug=true to a page's URL, then ReactiveMixin will conditionally expose a public state property that returns the component's state. You can then access the state in your browser's debug console.

Type: PlainObject

[internal.stateEffects](state, changed) method

Ask the component whether a state with a set of recently-changed fields implies that additional second-order changes should be applied to that state to make it consistent.

This method is invoked during a call to internal.setState to give all of a component's mixins and classes a chance to respond to changes in state. If one mixin/class updates state that it controls, another mixin/class may want to respond by updating some other state member that it controls.

This method should return a dictionary of changes that should be applied to the state. If the dictionary object is not empty, the internal.setState will apply the changes to the state, and invoke this stateEffects method again to determine whether there are any third-order effects that should be applied. This process repeats until all mixins/classes report that they have no additional changes to make.

See an example of how ReactiveMixin invokes the stateEffects to ensure state consistency.

Parameters:

  • state: PlainObjecta proposal for a new state
  • changed: ChangedFlagsthe set of fields changed in this latest proposal for the new state

Returns: PlainObject