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.
  • Optional componentDidMount method that runs after the component renders for the first time.
  • Optional componentDidUpdate method that runs after subsequent component renderings.

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/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.

To address these situations, the state of an Elix component is represented as an instance of a State class. Components and mixins can ask to be notified if specific members of the state ever change and, in response, can run code that requests that additional state updates be made so that the state will be consistent with their expectations. See the State documentation for details.

Detecting state changes

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 (below).

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 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) {
    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++;
      });
    }
    if (changed.value) {
      // When the value changes, show the value as the span's text.
      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.

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 two React-style lifecycle methods:

  • componentDidMount is invoked when your component has finished rendering for the first time.
  • componentDidUpdate is invoked whenever your component has completed a subsequent rerender.

ReactiveMixin does not provide componentWillUnmount; use the standard disconnectedCallback instead. Similarly, use the standard attributeChangedCallback instead of componentWillReceiveProps.

API

Used by classes AlertDialog, ArrowDirectionButton, AutoCompleteComboBox, AutoCompleteInput, AutoSizeTextarea, Backdrop, Button, CalendarDay, CalendarDayNamesHeader, CalendarDays, CalendarMonth, CalendarMonthNavigator, CalendarMonthYearHeader, Carousel, CarouselSlideshow, CarouselWithThumbnails, CenteredStrip, CenteredStripHighlight, CenteredStripOpacity, 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, Overlay, OverlayFrame, PageDot, Popup, PopupButton, PopupSource, ProgressSpinner, PullToRefresh, ReactiveElement, SeamlessButton, SelectableButton, Slideshow, SlideshowWithPlayControls, SlidingPages, SlidingStage, TabButton, Tabs, TabStrip, Thumbnail, 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: State

[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: objectdictionary 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 all internal render methods, then invokes componentDidMount (for first render) or componentDidUpdate (for subsequent renders).

[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: objectthe 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: State