React デベロッパー向け Lit

Lit とは

Lit は、任意のフレームワークで動作する高速で軽量なコンポーネントの開発に役立つ Google のオープンソース ライブラリで、共有可能なコンポーネントやアプリケーション、設計システムなどの構築に使用できます。

演習内容

以下について、React のコンセプトを Lit に変換する方法を学びます。

  • JSX とテンプレート
  • コンポーネントとプロパティ
  • 状態とライフサイクル
  • フック
  • 参照
  • 状態の仲介

達成目標

この Codelab を完了すると、React コンポーネントのコンセプトを Lit の対応するコンポーネントに変換できるようになります。

必要なもの

  • Chrome、Safari、Firefox、または Edge の最新バージョン。
  • HTML、CSS、JavaScript、Chrome DevTools に関する知識。
  • React に関する知識。
  • (上級者向け)最適な開発環境が必要な場合は、VS Code をダウンロードしてください。VS Code 用の lit-pluginNPM も必要です。

Lit の中心となるコンセプトと機能は多くの点で React と似ていますが、次のような違いがあります。

サイズが小さい

Lit はサイズが小さく、約 5 KB に gzip 圧縮され軽量化されています。これに対し、React と ReactDOM は 40 KB を超えます

圧縮し軽量化したバンドルのサイズを表す棒グラフ(KB 単位)。Lit の棒グラフは 5 KB、React と ReactDOM は 42.2 KB

処理が速い

Lit のテンプレート システム「lit-html」を React の VDOM と比較した公開ベンチマークでは、lit-html は React と比べ最低でも 8~10% 速く、一般的なユースケースでは 50% 以上速いという結果が出ています。

LitElement(Lit コンポーネントのベースクラス)により lit-html に最小限のオーバーヘッドが追加されますが、メモリ使用量、インタラクション、起動時間などのコンポーネントの機能を比較すると React のパフォーマンスを 16~30% 上回ります

Lit と React のパフォーマンス比較のグループ別棒グラフ(ミリ秒単位、低いほど高パフォーマンス)

ビルドが不要

ES モジュールのような新しいブラウザ機能とタグ付きのテンプレート リテラルにより、Lit はコンパイルなしで実行できます。つまり、スクリプトタグ、ブラウザ、サーバーがあれば、開発環境を設定して運用できます。

ES モジュールと最新の CDN(SkypackUNPKG など)があれば、NPM なしでも始められます。

必要な場合は、Lit コードを作成、最適化することも可能です。ネイティブ ES モジュールは、近年統合が進んでおり、標準の JavaScript であり、フレームワーク固有の CLI やビルド処理の必要がない Lit を使用するメリットは大きくなっています。

フレームワークに依存しない

Lit コンポーネントは、「Web Components」と呼ばれる一連のウェブ標準仕様に沿って構築されています。つまり、Lit で作成したコンポーネントは、現在だけでなく今後リリースされるフレームワークでも動作します。HTML 要素をサポートしているということは、Web Components もサポートしているということです。

フレームワークの相互運用において唯一問題が生じるのは、フレームワークで DOM のサポートが制限されている場合です。React はこうしたフレームワークの 1 つで、参照によるエスケープ ハッチが許可されていますが、こうした手法は効率的ではありません。

Lit チームでは、@lit-labs/react という実験的なプロジェクトに取り組んでおり、Lit コンポーネントを自動解析して React ラッパーを生成することで、参照の使用を回避できるよう目指しています。

また、Custom Elements Everywhere では、カスタム要素が適切に動作するフレームワークやライブラリを確認できます。

TypeScript の高度なサポート

Lit コードはすべて JavaScript で記述できますが、Lit 自体は TypeScript で記述されているため、デベロッパーも TypeScript を使用することが推奨されています。

Lit チームでは Lit コミュニティと協力し、lit-analyzerlit-plugin を使用した開発およびビルド時に Lit テンプレートでの TypeScript の型チェックや入力支援を行うプロジェクトをサポートしています。

IDE で型チェックにより数値ではなくブール値が誤って設定されていることが検出された場面のスクリーンショット

IDE で入力支援により候補が表示されている場面のスクリーンショット

ブラウザに組み込みの DevTools

Lit コンポーネントは DOM の HTML 要素です。つまり、コンポーネントを検証するために、ブラウザにツールや拡張機能をインストールする必要はありません

DevTools を開いて要素を選択すれば、そのプロパティや状態を確認できます。

を、$0.value は「hello world」を、$0.outlined は true をそれぞれ返し、{$0} にはプロパティが展開表示されています" class="l10n-relative-url-src" l10n-attrs-original-order="alt,src,class" src="https://codelabs.developers.google.com/codelabs/lit-2-for-react-devs/./img/browser-tools.png" />

サーバーサイド レンダリング(SSR)を考慮した設計

Lit 2 は SSR に対応できるように構築されています。この Codelab を作成した時点では、SSR ツールの安定版はまだリリースされていませんが、サーバーサイドでレンダリングされるコンポーネントは Google サービス全体ですでに実装されています。これらのツールは GitHub で近日中に一般リリースされる予定です。

それまでの間、進捗状況はこちらでご確認いただけます。

導入しやすい

Lit は大きな負担なく使用できます。Lit で作成したコンポーネントを既存のプロジェクトに追加することも可能です。Web Components は他のフレームワークでも動作するため、必要なければ、アプリ全体を一度に変換しなくても構いません

アプリ全体を Lit で作成していて、フレームワークを別のものに変更したい場合は、現在の Lit アプリケーションを新しいフレームワーク内に置いて、必要な要素だけを新しいフレームワークのコンポーネントに移行できます。

また、Web Components への出力は、最新の多くのフレームワークでサポートされているので、通常は Lit 要素内に含めることができます。

この Codelab の演習は、次の 2 つの方法で行うことができます。

  • ブラウザを使用してすべてオンラインで行う
  • (上級者向け)VS Code を使用してローカルのパソコンで行う

コードへのアクセス

この Codelab では、次のような Lit 環境へのリンクが設けられています。

コードのチェックポイント

この環境は、ブラウザで完全に動作するコード サンドボックスです。TypeScript や JavaScript のファイルをコンパイルして実行できるだけでなく、次のようなノード モジュールへの読み込みを自動的に解決することも可能です。

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

これらのチェックポイントを出発点に、Lit 環境でチュートリアル全体を実施できます。VS Code を使用する場合は、これらのチェックポイントから任意のステップの開始用コードをダウンロードしたり、そのコードを使って製品をチェックしたりできます。

Lit 環境の UI

ファイル選択ツールのタブバーに「セクション 1」、コード編集セクションに「セクション 2」、出力プレビューに「セクション 3」、プレビューの再読み込みボタンに「セクション 4」のラベルが付いています

Lit 環境の UI のスクリーンショットで、この Codelab で使用するセクションがハイライト表示されています。

  1. ファイル選択ツール。プラスボタンをご確認ください。
  2. ファイル エディタ。
  3. コード プレビュー。
  4. 再読み込みボタン。
  5. ダウンロード ボタン。

VS Code の設定(上級者向け)

この VS Code の設定を使用すると、次の機能を使用できます。

  • テンプレートの型チェック
  • テンプレートの入力支援とオートコンプリート

NPM と VS Code(および lit-plugin プラグイン)がインストール済みで、これらの環境の使用方法をご存知の場合は、次の手順でプロジェクトをダウンロードして開始できます。

  • ダウンロード ボタンを押す
  • tar ファイルの中身をディレクトリに展開する
  • (TS の場合)ES モジュールを出力する quick tsconfig と ES2015 以降を設定する
  • ベアモジュール指定子(Lit チームの推奨は @web/dev-server)を解決できる開発サーバーをインストールする
  • 開発サーバーを実行し、ブラウザを開く(@web/dev-serve の場合は web-dev-server --node-resolve --watch --open を使用できます)

このセクションでは、Lit のテンプレートの基本を学びます。

JSX と Lit テンプレート

React では、JavaScript の構文を拡張した JSX を使用して、JavaScript コードでテンプレートを簡単に記述できます。Lit テンプレートも同じような用途で使用し、コンポーネントの UI をその状態の関数として表すことができます。

基本的な構文

コードのチェックポイント

React では、JSX の「hello world」を次のようにレンダリングします。

import 'react';
import ReactDOM from 'react-dom';

const name = 'Josh Perez';
const element = (
  <>
    <h1>Hello, {name}</h1>
    <div>How are you?</div>
  </>
);

ReactDOM.render(
  element,
  mountNode
);

上の例では 2 つの要素と「name」変数を使用しています。Lit では、次のように記述します。

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Lit テンプレートでは、テンプレートの複数の要素をグループ化する際に React Fragment は必要ありません。

Lit では、テンプレートは html タグ付きのテンプレート リテラル(Literal)でラップされます。ちなみにこれが Lit の名称の由来です。

テンプレートの値

Lit テンプレートには、TemplateResult という形で、他の Lit テンプレートを組み込むことができます。たとえば、name を斜体(<i>)タグでラップし、それをさらにタグ付きのテンプレート リテラル N.B. でラップします。その際は、一重引用(')ではなく逆引用符(`)を必ず使用してください。

import {html, render} from 'lit';

const name = html`<i>Josh Perez</i>`;
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Lit の TemplateResult では、配列、文字列、他の TemplateResult、およびディレクティブを受け取ることができます。

コードのチェックポイント

演習として、次の React コードを Lit に変換してみましょう。

const itemsToBuy = [
  <li>Bananas</li>,
  <li>oranges</li>,
  <li>apples</li>,
  <li>grapes</li>
];
const element = (
  <>
    <h1>Things to buy:</h1>
    <ol>
      {itemsToBuy}
    </ol>
  </>);

ReactDOM.render(
  element,
  mountNode
);

解答:

import {html, render} from 'lit';

const itemsToBuy = [
  html`<li>Bananas</li>`,
  html`<li>oranges</li>`,
  html`<li>apples</li>`,
  html`<li>grapes</li>`
];
const element = html`
  <h1>Things to buy:</h1>
  <ol>
    ${itemsToBuy}
  </ol>`;

render(
  element,
  mountNode
);

プロパティを渡して設定する

コードのチェックポイント

JSX と Lit の構文の主な違いの 1 つは、データ バインディング構文です。バインディングを使用した次の React の input を例に説明します。

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      disabled={disabled}
      className={`static-class ${myClass}`}
      defaultValue={value}/>;

ReactDOM.render(
  element,
  mountNode
);

上の例では、input に対して以下の処理を行うよう定義しています。

  • disabled を定義済みの変数(この例では false)に設定する
  • クラスを static-class と変数(この例では "static-class my-class")に設定する
  • デフォルト値を設定する

Lit では、次のように記述します。

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      ?disabled=${disabled}
      class="static-class ${myClass}"
      .value=${value}>`;

render(
  element,
  mountNode
);

Lit では、まず disabled 属性を切り替えるため、ブール値のバインディングを追加します。

次に、className ではなく class 属性に直接バインドします。class 属性には複数のバインディングを追加できます。ただし、クラスの切り替えに宣言型ヘルパーの classMap ディレクティブを使用している場合は除きます。

最後に、input に対して value プロパティを設定します。React とは異なり、input 要素は読み取り専用には設定されず、input のネイティブの実装と動作に従います。

Lit のプロパティ バインディング構文

html`<my-element ?attribute-name=${booleanVar}>`;
  • ? プレフィックスは要素の属性を切り替えるバインディング構文
  • inputRef.toggleAttribute('attribute-name', booleanVar) に相当
  • disabled を使用する要素に有効(inputElement.hasAttribute('disabled') === true により、disabled="false" は引き続き DOM で true と読み取られるため)
html`<my-element .property-name=${anyVar}>`;
  • . プレフィックスは要素のプロパティを設定するバインディング構文
  • inputRef.propertyName = anyVar に相当
  • オブジェクト、配列、クラスなどの複雑なデータを渡すのに最適
html`<my-element attribute-name=${stringVar}>`;
  • 要素の属性にバインド
  • inputRef.setAttribute('attribute-name', stringVar) に相当
  • 基本的な値、スタイルルール セレクタ、クエリセレクタに最適

ハンドラを渡す

コードのチェックポイント

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      onClick={() => console.log('click')}
      onChange={e => console.log(e.target.value)} />;

ReactDOM.render(
  element,
  mountNode
);

上の例では、input に対して以下の処理を行うよう定義しています。

  • input のクリック時に「click」と記録する
  • ユーザーが文字を入力したときに input の値を記録する

Lit では、次のように記述します。

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      @click=${() => console.log('click')}
      @input=${e => console.log(e.target.value)}>`;

render(
  element,
  mountNode
);

Lit では、まず @click を使用して click イベントにリスナーを追加します。

次に、onChange を使用する代わりに、<input> のネイティブの input イベントにバインドします。これは、ネイティブの change イベントblur の発生時にのみ呼び出されるためです(React ではこれらのイベントは抽象化されます)。

Lit のイベント ハンドラ構文

html`<my-element @event-name=${() => {...}}></my-element>`;
  • @ プレフィックスはイベント リスナーのバインディング構文
  • inputRef.addEventListener('event-name', ...) に相当
  • ネイティブの DOM イベント名を使用

このセクションでは、Lit のクラス コンポーネントと関数について学びます。状態とフックについては、後述のセクションで詳しく確認します。

クラス コンポーネントと LitElement

コードのチェックポイント(TS)コードのチェックポイント(JS)

React のクラス コンポーネントに相当する Lit の要素は LitElement です。Lit の「リアクティブ プロパティ」のコンセプトは、React のプロパティと状態を組み合わせたものになります。たとえば次のコードがあるとします。

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''};
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

上の例の React コンポーネントでは、以下の処理を行っています。

  • name をレンダリングする
  • name のデフォルト値を空の文字列("")に設定する
  • name"Elliott" に割り当て直す

LitElement では、次のように記述します。

TypeScript の場合:

import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  @property({type: String})
  name = '';

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

JavaScript の場合:

import {LitElement, html} from 'lit';

class WelcomeBanner extends LitElement {
  static get properties() {
    return {
      name: {type: String}
    }
  }

  constructor() {
    super();
    this.name = '';
  }

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

customElements.define('welcome-banner', WelcomeBanner);

HTML ファイルの場合:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

上の例で行っている処理の再確認:

@property({type: String})
name = '';
  • パブリックなリアクティブ プロパティ(コンポーネントのパブリック API の一部)を定義する
  • コンポーネントの属性(デフォルト)とプロパティをエクスポーズする
  • コンポーネントの属性(文字列)を値に変換する方法を定義する
static get properties() {
  return {
    name: {type: String}
  }
}
  • @property TS デコレータと同じ働きをするが、JavaScript でネイティブに実行される
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • リアクティブ プロパティが変更されるたびに呼び出される
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • HTML 要素タグ名をクラスの定義に関連付ける
  • カスタム要素の標準仕様に沿って、タグ名にハイフン(-)を含める必要がある
  • LitElement の this は、カスタム要素のインスタンス(この例では <welcome-banner>)を参照する
customElements.define('welcome-banner', WelcomeBanner);
  • @customElement TS デコレータに相当する JavaScript
<head>
  <script type="module" src="./index.js"></script>
</head>
  • カスタム要素の定義を読み込む
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • カスタム要素をページに追加する
  • name プロパティを 'Elliott' に設定する

関数コンポーネント

コードのチェックポイント

Lit では、JSX やプリプロセッサを使用しないため、関数コンポーネントに相当するものはありません。しかしながら、プロパティを取り込み、そのプロパティに基づいて DOM をレンダリングする関数は簡単に作成できます。たとえば、次のコードがあるとします。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

Lit では、次のように記述します。

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'Elliott'}),
  document.body.querySelector('#root')
);

このセクションでは、Lit の状態とライフサイクルについて学びます。

状態

Lit の「リアクティブ プロパティ」のコンセプトは、React の状態とプロパティを組み合わせたものになります。リアクティブ プロパティでは、変更時にコンポーネントのライフサイクルをトリガーできます。リアクティブ プロパティには 2 つの種類があります。

パブリックなリアクティブ プロパティ

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.name !== nextProps.name) {
      this.setState({name: nextProps.name})
    }
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';

class MyEl extends LitElement {
  @property() name = 'there';
}
  • @property で定義する
  • React のプロパティと状態に似ているが変更可能
  • コンポーネントのユーザーによってアクセスおよび設定されるパブリック API

内部のリアクティブの状態

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';

class MyEl extends LitElement {
  @state() name = 'there';
}
  • @state で定義する
  • React の状態に似ているが変更可能
  • 通常はコンポーネントまたはサブクラス内からアクセスされるプライベートな内部の状態

ライフサイクル

Lit のライフサイクルは React のライフサイクルとよく似ていますが、重要な違いがいくつかあります。

constructor

// React (js)
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this._privateProp = 'private';
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) counter = 0;
  private _privateProp = 'private';
}

// Lit (js)
class MyEl extends LitElement {
  static get properties() {
    return { counter: {type: Number} }
  }
  constructor() {
    this.counter = 0;
    this._privateProp = 'private';
  }
}
  • 相当する Lit の要素も constructor
  • super の呼び出しには何も渡さなくてよい
  • 呼び出し元(これらに限りません):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • アップグレードされていないタグ名がページに存在する場合は、定義が読み込まれて @customElement または customElements.define で登録される
  • React の constructor と同様の働き

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • 相当する Lit の要素も render
  • TemplateResultstring など、レンダリング可能な任意の結果を返すことができる
  • React と同様、render() は純粋な関数である必要がある
  • createRenderRoot() から返されるノードにレンダリングされる(デフォルトは ShadowRoot

componentDidMount

Lit で componentDidMount と同様の働きをするのは、firstUpdatedconnectedCallback のライフサイクル コールバックの組み合わせです。

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
  this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • コンポーネントのテンプレートがコンポーネントのルートに初めてレンダリングされたときに呼び出される
  • 要素が接続されている場合にのみ呼び出される(たとえば、そのノードが DOM ツリーに追加されるまで document.createElement('my-component') を介した呼び出しは行われない)
  • レンダリングする DOM が必要なコンポーネントを設定するのに最適
  • React の componentDidMount と異なり、firstUpdated のリアクティブ プロパティが変更されるとレンダリングが再度トリガーされるが、ブラウザでは通常、変更が同じフレームにバッチ処理される。ルートの DOM へのアクセスを必要としない変更の場合、通常は willUpdate に渡される

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • カスタム要素が DOM ツリーに挿入されるたびに呼び出される
  • React コンポーネントと異なり、カスタム要素は DOM からデタッチされても破棄されないため、複数回「接続」できる
  • DOM の再初期化や、切断時に削除されたイベント リスナーの再アタッチに有効
  • 注: connectedCallbackfirstUpdated の前に呼び出されることがあるため、初回の呼び出し時には DOM を使用できない可能性がある

componentDidUpdate

// React
componentDidUpdate(prevProps) {
  if (this.props.title !== prevProps.title) {
    this._chart.setTitle(this.props.title);
  }
}

// Lit (ts)
updated(prevProps: PropertyValues<this>) {
  if (prevProps.has('title')) {
    this._chart.setTitle(this.title);
  }
}
  • 相当する Lit の要素は updated(英語の「update」の過去形を使用)
  • React と異なり、updated は初回レンダリング時にも呼び出される
  • React の componentDidUpdate と同様の働き

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • 相当する Lit の同様の要素は disconnectedCallback
  • React コンポーネントと異なり、カスタム要素を DOM からデタッチしてもコンポーネントは破棄されない
  • componentWillUnmount と異なり、disconnectedCallback は要素がツリーから削除されたに呼び出される
  • ルート内の DOM は引き続きルートのサブツリーにアタッチされる
  • イベント リスナーやメモリリークを起こしやすい参照を削除して、ブラウザでコンポーネントのガベージ コレクションを行えるようにするのに有効

演習

コードのチェックポイント(TS)コードのチェックポイント(JS)

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

上の例では、シンプルな時計を表示して、次の処理を行っています。

  • 「Hello World! It is」をレンダリングした後、時刻を表示する
  • 時計を毎秒更新する
  • マウントが解除されたら、tick を呼び出してタイマーを停止する

まず、コンポーネントのクラスを宣言します。

// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
}

// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
}

customElements.define('lit-clock', LitClock);

次に、date を初期化して、@state で内部のリアクティブ プロパティであることを宣言します。これは、コンポーネントのユーザーが date を直接設定することはないためです。

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state() // declares internal reactive prop
  private date = new Date(); // initialization
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      // declares internal reactive prop
      date: {state: true}
    }
  }

  constructor() {
    super();
    // initialization
    this.date = new Date();
  }
}

customElements.define('lit-clock', LitClock);

次に、テンプレートをレンダリングします。

// Lit (JS & TS)
render() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

次に、tick メソッドを実装します。

tick() {
  this.date = new Date();
}

続けて、componentDidMount を実装します。繰り返しになりますが、Lit でこれに相当するのは firstUpdatedconnectedCallback の組み合わせです。このコンポーネントの場合、setIntervaltick を呼び出す際にルート内の DOM にアクセスする必要はありません。また、要素がドキュメント ツリーから削除されるときはタイマーが停止されるので、再アタッチする場合はタイマーをもう一度開始する必要があります。したがって、ここでは connectedCallback の方が適しています。

// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1; // initialize timerId for TS

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  ...
}

// Lit (JS)
constructor() {
  super();
  // initialization
  this.date = new Date();
  this.timerId = -1; // initialize timerId for JS
}

connectedCallback() {
  super.connectedCallback();
  this.timerId = setInterval(
    () => this.tick(),
    1000
  );
}

最後に、タイマーを停止して、要素がドキュメント ツリーから切断された後に tick が実行されないようにします。

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

ここまでのコードをまとめると、次のようになります。

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      date: {state: true}
    }
  }

  constructor() {
    super();
    this.date = new Date();
  }

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

customElements.define('lit-clock', LitClock);

このセクションでは、React のフックのコンセプトを Lit に変換する方法について学びます。

React のフックのコンセプト

React のフックは、関数コンポーネントで状態を「フック」できるようにする機能です。フックを使用すると、さまざまなメリットがあります。

  • ステートフルなロジックを簡単に再利用できる
  • より小さな関数にコンポーネントを分割できる

さらに、関数ベースのコンポーネントを主に使用することで、React のクラスベースの構文で生じる次のような問題を解決できます。

  • propsconstructor から super に渡す必要がある
  • constructor でのプロパティの初期化が整理されていない
    • 当時 React チームがフック導入の理由として挙げたものの、ES2019 で解決済み
  • this がコンポーネントを参照しなくなったことにより生じる問題

Lit における React のフックのコンセプト

コンポーネントとプロパティ」のセクションで学んだように、Lit では関数からカスタム要素を作成することはできませんが、LitElement を使用することで、React のクラス コンポーネントに関する主な問題のほとんどに対処できます。たとえば、次のコードがあるとします。

// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';

class MyEl extends React.Component {
  constructor(props) {
    super(props); // Leaky implementation
    this.state = {count: 0};
    this._chart = null; // Deemed messy
  }

  render() {
    return (
      <>
        <div>Num times clicked {count}</div>
        <button onClick={this.clickCallback}>click me</button>
      </>
    );
  }

  clickCallback() {
    // Errors because `this` no longer refers to the component
    this.setState({count: this.count + 1});
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) count = 0; // No need for constructor to set state
  private _chart = null; // Public class fields introduced to JS in 2019

  render() {
    return html`
        <div>Num times clicked ${count}</div>
        <button @click=${this.clickCallback}>click me</button>`;
  }

  private clickCallback() {
    // No error because `this` refers to component
    this.count++;
  }
}

Lit では、こうした問題に次のように対処しています。

  • constructor は引数を取らない
  • @event バインディングはすべて this に自動バインドする
  • ほとんどのケースで this はカスタム要素の参照を指す
  • クラスのプロパティをクラスの構成要素としてインスタンス化できる(これに伴い、コンストラクタベースの実装を削除)

リアクティブ コントローラ

コードのチェックポイント(TS)コードのチェックポイント(JS)

Lit では、フックの主要なコンセプトは、リアクティブ コントローラとして実現されています。リアクティブ コントローラ パターンを使用すると、ステートフルなロジックを共有したり、コンポーネントをより多くの小さなモジュールに分割したり、要素の更新ライフサイクルにフックしたりできます。

リアクティブ コントローラは、LitElement などのコントローラ ホストの更新ライフサイクルにフックできるオブジェクト インターフェースです。

ReactiveControllerreactiveControllerHost のライフサイクルは次のとおりです。

interface ReactiveController {
  hostConnected(): void;
  hostUpdate(): void;
  hostUpdated(): void;
  hostDisconnected(): void;
}
interface ReactiveControllerHost {
  addController(controller: ReactiveController): void;
  removeController(controller: ReactiveController): void;
  requestUpdate(): void;
  readonly updateComplete: Promise<boolean>;
}

リアクティブ コントローラを作成し、addController を使ってホストにアタッチすることで、コントローラのライフサイクルがホストのライフサイクルとともに呼び出されます。「状態とライフサイクル」のセクションの時計を例に説明します。

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

上の例では、シンプルな時計を表示して、次の処理を行っています。

  • 「Hello World! It is」をレンダリングした後、時刻を表示する
  • 時計を毎秒更新する
  • マウントが解除されたら、tick を呼び出してタイマーを停止する

コンポーネントの基盤の作成

まず、コンポーネントのクラスを宣言してから、render 関数を追加します。

// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';

class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

コントローラの作成

次に、clock.ts に切り替えて、ClockController のクラスを作成し、constructor を設定します。

// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';

export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  private tick() {
  }

  hostDisconnected() {
  }

  // Will not be used but needed for TS compilation
  hostUpdate() {};
  hostUpdated() {};
}

// Lit (JS) - clock.js
export class ClockController {
  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  tick() {
  }

  hostDisconnected() {
  }
}

リアクティブ コントローラは、ReactiveController インターフェースを共有している限り、どのような方法でも作成できます。しかしながら、多くの基本的なケースでは、クラスで constructor を使用して、ReactiveControllerHost インターフェースだけでなく、コントローラの初期化に必要な他のプロパティを取り込めるようにすることが推奨されます。

ここで、React のライフサイクル コールバックをコントローラ コールバックに変換する必要があります。つまり、以下の変換を行います。

  • componentDidMount
    • LitElement の connectedCallback に変換
    • コントローラの hostConnected に変換
  • ComponentWillUnmount
    • LitElement の disconnectedCallback に変換
    • コントローラの hostDisconnected に変換

React のライフサイクルを Lit のライフサイクルに変換する方法について詳しくは、「状態とライフサイクル」のセクションをご覧ください。

次に、「状態とライフサイクル」のセクションの例で行ったとおり、hostConnected コールバックと tick メソッドを実装して、hostDisconnected でタイマーを停止します。

// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;
  private interval = 0;
  date = new Date();

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  private tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }

  hostUpdate() {};
  hostUpdated() {};
}

// Lit (JS) - clock.js
export class ClockController {
  interval = 0;
  host;
  date = new Date();

  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

コントローラの使用

作成した時計コントローラを使用するには、コントローラを読み込み、index.ts または index.js でコンポーネントを更新します。

// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';

@customElement('my-element')
class MyElement extends LitElement {
  private readonly clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';

class MyElement extends LitElement {
  clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

コントローラを使用するには、コントローラ ホストへの参照(<my-element> コンポーネント)を渡してコントローラをインスタンス化してから、render メソッドでコントローラを使用する必要があります。

コントローラでの再レンダリングのトリガー

以下のコードでは、時刻は表示されますが、更新されません。これは、コントローラで日付が毎秒設定されているものの、ホストで更新が行われていないためです。dateClockController クラスで変更されていて、コンポーネントでなくなっていることがその原因です。このため、date をコントローラに設定したら、host.requestUpdate() を使用して、ホストに更新ライフサイクルを実行するよう指示する必要があります。

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

これで、時計の時刻が更新されるようになります。

一般的な用途とフックの詳細な比較については、「上級者向けトピック - フック」のセクションをご覧ください。

このセクションでは、Lit でスロットを使用して子を管理する方法について学びます。

スロットと子

コードのチェックポイント

スロットを使用すると、コンポーネントをネストして、コンポジションを作成できます。

React では、プロパティを介して子が継承されます。デフォルトのスロットは props.children で、render 関数を使ってその場所を定義します。たとえば、次のコードがあるとします。

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

props.children は HTML 要素ではなく React コンポーネントであることに注意してください。

Lit では、子はレンダリング関数内でスロット要素を使って作成します。子の継承は React と同じ方法では行われません。Lit では、子はスロットにアタッチされる HTML 要素であり、このアタッチのことを投影と呼びます。

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

複数のスロット

コードのチェックポイント

React では、複数のスロットを追加することは、継承されるプロパティが増えることと本質的に同じです。

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

Lit でも同様に、<slot> 要素を追加すると、作成されるスロットが増えます。複数のスロットを定義する場合は、name 属性を使用します(<slot name="slot-name">)。これにより、子は割り当て先のスロットを宣言できるようになります。

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <header>
          <slot name="headerChildren"></slot>
        </header>
        <section>
          <slot name="sectionChildren"></slot>
        </section>
      </article>
   `;
  }
}

デフォルトのスロットのコンテンツ

スロットにノードが投影されていない場合、スロットにはサブツリーが表示されます。スロットにノードが投影されている場合は、サブツリーの代わりに、投影されているノードが表示されます。

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot name="slotWithDefault">
            <p>
             This message will not be rendered when children are attached to this slot!
            <p>
          </slot>
        </div>
      </section>
   `;
  }
}

スロットへの子の割り当て

コードのチェックポイント

React では、コンポーネントのプロパティを介して、スロットに子を割り当てます。次の例では、React の要素を headerChildrensectionChildren のプロパティに渡しています。

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

Lit では、slot 属性を使用して、スロットに子を割り当てます。

@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
  render() {
    return html`
      <my-article>
        <h3 slot="headerChildren">
          Extry, Extry! Read all about it!
        </h3>
        <p slot="sectionChildren">
          Children are composed with slots in Lit!
        </p>
      </my-article>
   `;
  }
}

デフォルトのスロット(例: <slot>)がなく、カスタム要素の子(例: <div slot="foo">)の slot 属性に一致する name 属性が指定されたスロット(例: <slot name="foo">)もない場合、そのノードは投影されず、表示されません。

場合によっては、HTML 要素の API にアクセスする必要があります。

このセクションでは、Lit で要素の参照を取得する方法を学びます。

React の参照

コードのチェックポイント(TS)コードのチェックポイント(JS)

React コンポーネントは、一連の関数呼び出しにトランスパイルされ、呼び出されると仮想 DOM を作成します。この仮想 DOM は ReactDOM によって解釈、実行され、HTML 要素をレンダリングします。

React における参照は、生成された HTML 要素を格納するメモリ内のスペースを指します。

const RefsExample = (props) => {
 const inputRef = React.useRef(null);
 const onButtonClick = React.useCallback(() => {
   inputRef.current?.focus();
 }, [inputRef]);

 return (
   <div>
     <input type={"text"} ref={inputRef} />
     <br />
     <button onClick={onButtonClick}>
       Click to focus on the input above!
     </button>
   </div>
 );
};

上の例の React コンポーネントでは、以下の処理を行っています。

  • 空のテキスト入力欄と、テキスト付きのボタンをレンダリングする
  • ボタンのクリック時に入力欄にフォーカスする

初回レンダリング後、React では ref 属性を介して、生成された HTMLInputElementinputRef.current を設定します。

Lit の @query を使用した「参照」

Lit はブラウザに近く、ネイティブのブラウザ機能に対して非常に薄い抽象化層を作成します。

React の refs に相当する Lit の要素は、@query@queryAll デコレータによって返される HTML 要素です。

@customElement("my-element")
export class MyElement extends LitElement {
  @query('input') // Define the query
  inputEl!: HTMLInputElement; // Declare the prop

  // Declare the click event listener
  onButtonClick() {
    // Use the query to focus
    this.inputEl.focus();
  }

  render() {
    return html`
      <input type="text"></input>
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

上の例では、Lit コンポーネントで以下の処理を行っています。

  • MyElement@query デコレータを使用してプロパティを定義する(HTMLInputElement のゲッターを作成する)
  • onButtonClick というクリック イベント コールバックを宣言してアタッチする
  • ボタンのクリック時に入力欄にフォーカスする

JavaScript では、@query@queryAll のデコレータはそれぞれ querySelectorquerySelectorAll を実行します。これは JavaScript の @query('input') inputEl!: HTMLInputElement; に相当します。

get inputEl() {
  return this.renderRoot.querySelector('input');
}

Lit コンポーネントで render メソッドのテンプレートを my-element のルートにコミットすると、@query デコレータによって、レンダリング ルートで最初に検出された input 要素を inputEl から返せるようになります。@query で、指定された要素を検出できない場合は null が返されます。

レンダリング ルートに複数の input 要素がある場合、@queryAll はノードのリストを返します。

このセクションでは、Lit コンポーネント間で状態を仲介する方法を学びます。

再利用可能なコンポーネント

コードのチェックポイント

React は、トップダウンのデータフローによる関数レンダリング パイプラインを模しており、親はプロパティを介して子に状態を渡し、子はプロパティで渡されたコールバックを介して親と通信します。

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;

  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

上の例の React コンポーネントでは、以下の処理を行っています。

  • props.step に基づいてラベルを作成する
  • ボタンに + ステップまたは - ステップのラベルをレンダリングする
  • クリック時に props.step を引数として props.addToCounter を呼び出し、親コンポーネントを更新する

Lit でコールバックを渡すことは可能ですが、その使用パターンは異なります。上の例の React コンポーネントは、以下の例のような Lit コンポーネントとして記述することができます。

@customElement('counter-button')
export class CounterButton extends LitElement {
  @property({type: Number}) step: number = 0;

  onClick() {
    const event = new CustomEvent('update-counter', {
      bubbles: true,
      detail: {
        step: this.step,
      }
    });

    this.dispatchEvent(event);
  }

  render() {
    const label = this.step < 0
      ? `- ${-1 * this.step}`  // "- 1"
      : `+ ${this.step}`;      // "+ 1"

    return html`
      <button @click=${this.onClick}>${label}</button>
    `;
  }
}

上の例の Lit コンポーネントでは、以下の処理を行っています。

  • リアクティブ プロパティ step を作成する
  • クリック時に要素の step 値を保持する update-counter というカスタム イベントをディスパッチする

ブラウザのイベントは、子要素から親要素へとバブリングされます。これにより、子はインタラクション イベントや状態の変化をブロードキャストできます。React では基本的に状態が逆方向に渡されるため、React コンポーネントが Lit コンポーネントと同じようにイベントをディスパッチしてリッスンすることはまれです。

ステートフル コンポーネント

コードのチェックポイント

React では、フックを使用して状態を管理するのが一般的です。MyCounter コンポーネントは、CounterButton コンポーネントを再利用して作成できます。addToCounterCounterButton の両方のインスタンスにどのように渡しているかに注意してください。

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

上の例では、以下の処理を行っています。

  • count 状態を作成する
  • count 状態に数値を追加するコールバックを作成する
  • CounterButtonaddToCounter を使用して、クリックのたびに step ずつ count を更新する

MyCounter と同様の実装を Lit でも実現できます。addToCountercounter-button に渡していないことに注意してください。代わりに、コールバックを親要素の @update-counter イベントに対するイベント リスナーとしてバインドします。

@customElement("my-counter")
export class MyCounter extends LitElement {
  @property({type: Number}) count = 0;

  addToCounter(e: CustomEvent<{step: number}>) {
    // Get step from detail of event or via @query
    this.count += e.detail.step;
  }

  render() {
    return html`
      <div @update-counter="${this.addToCounter}">
        <h3>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

上の例では、以下の処理を行っています。

  • count というリアクティブ プロパティを作成し、値が変更されたときにコンポーネントを更新する
  • addToCounter コールバックを @update-counter イベント リスナーにバインドする
  • update-counter イベントの detail.step の値を追加して count を更新する
  • step 属性を介して、counter-buttonstep 値を設定する

Lit では、リアクティブ プロパティを使用して親から子に変更をブロードキャストする手法が一般的です。同様に、ブラウザのイベント システムを使用して、ボトムアップで詳細をバブリングする手法も推奨されます。

この手法はベスト プラクティスに沿っているだけでなく、Web Components のクロス プラットフォーム サポートを実現するという Lit の目標にも即しています。

このセクションでは、Lit のスタイル設定について学びます。

スタイル設定

Lit では、複数の方法で要素のスタイルを設定できるほか、組み込みのソリューションも用意されています。

インライン スタイル

コードのチェックポイント

Lit では、インライン スタイルとそのバインディングがサポートされています。

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1 style="color:orange;">This text is orange</h1>
        <h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

上の例では、インライン スタイルを使って、2 つの見出しをそれぞれ指定しています。

次に、オレンジ色のテキストに border: 1px solid black をバインドします。

<h1 style="color:orange;${'border: 1px solid black;'}">This text is orange</h1>

スタイル文字列を毎回計算しなければならないのは手間がかかるため、Lit ではそれをサポートするディレクティブが提供されています。

styleMap

styleMap ディレクティブを使用すると、JavaScript で簡単にインライン スタイルを設定できます。たとえば、次のコードがあるとします。

コードのチェックポイント

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map';

@customElement('my-element')
class MyElement extends LitElement {
  @property({type: String})
  color = '#000'

  render() {
    // Define the styleMap
    const headerStyle = styleMap({
      'border-color': this.color,
    });

    return html`
      <div>
        <h1
          style="border-style:solid;
          <!-- Use the styleMap -->
          border-width:2px;${headerStyle}">
          This div has a border color of ${this.color}
        </h1>
        <input
          type="color"
          @input=${e => (this.color = e.target.value)}
          value="#000">
      </div>
    `;
  }
}

上の例では、以下の処理を行っています。

  • 枠線とカラー選択ツールを使用して h1 を表示する
  • border-color をカラー選択ツールの値に変更する

さらに、styleMap を使用して h1 のスタイルを設定しています。styleMap で使用する構文は、React の style 属性のバインディング構文と同様です。

CSSResult

コードのチェックポイント

コンポーネントのスタイル設定では、css タグ付きのテンプレート リテラルを使用することをおすすめします。

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

const ORANGE = css`orange`;

@customElement('my-element')
class MyElement extends LitElement {
  static styles = [
    css`
      #orange {
        color: ${ORANGE};
      }

      #purple {
        color: rebeccapurple;
      }
    `
  ];

  render() {
    return html`
      <div>
    <h1 id="orange">This text is orange</h1>
        <h1 id="purple">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

上の例では、以下の処理を行っています。

  • バインディングを使って CSS タグ付きのテンプレート リテラルを宣言する
  • ID を使って 2 つの h1 の色を設定する

css テンプレート タグを使用すると、次のようなメリットがあります。

  • インスタンスごとではなく、クラスごとに 1 回解析される
  • モジュールの再利用を考慮して実装できる
  • スタイルを専用のファイルに簡単に切り離すことができる
  • CSS カスタム プロパティのポリフィルと互換性がある

また、index.html<style> タグにも注意してください。

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

Lit では、コンポーネントのスタイルのスコープはそのルートに設定されます。このため、スタイルがスコープ外に適用されることはありません。コンポーネントにスタイルを渡す場合は、Lit のスタイルのスコープに適用できる CSS カスタム プロパティを使用することが推奨されます。

スタイルタグ

テンプレートで <style> タグをインライン編集することもできます。ブラウザではこれらのスタイルタグの重複は除去されますが、テンプレートに配置すると、コンポーネント インスタンスごとに解析されるようになります。これは、クラスごとに解析される css タグ付きのテンプレートとは対照的です。また、ブラウザでの CSSResult の重複除去が高速化されます。

テンプレートで <link rel="stylesheet"> を使用してスタイルを設定することも可能ですが、コンテンツのスタイル設定の遅れによるちらつき(FOUC)を引き起こす可能性があるため推奨されません。

JSX とテンプレート

Lit と仮想 DOM

lit-html には、個々のノードの差分を抽出する従来の仮想 DOM は含まれません。代わりに、ES2015 のタグ付きのテンプレート リテラルの仕様に固有のパフォーマンス機能が使用されます。タグ付きのテンプレート リテラルは、タグ関数がアタッチされたテンプレート リテラル文字列です。

次にテンプレート リテラルの例を示します。

const str = 'string';
console.log(`This is a template literal ${str}`);

次にタグ付きのテンプレート リテラルの例を示します。

const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true

上の例では、タグは tag 関数となっており、f 関数はタグ付きのテンプレート リテラルの呼び出しを返します。

Lit のパフォーマンスが優れている主な理由は、タグ関数に渡される文字列配列で同じポインタが使用されている(2 つ目の console.log を参照)という点です。ブラウザでは同じテンプレート リテラル(AST の同じ場所にあるもの)が使用されるので、タグ関数の呼び出しのたびに新しい strings 配列が再作成されることはありません。Lit ではバインド、解析、テンプレートのキャッシュ時にこうした機能を活用できるため、実行時に差分オーバーヘッドはほとんど生じません。

Lit の優れたパフォーマンスは、タグ付きのテンプレート リテラルがブラウザでこのように動作することによって、もたらされています。従来の多くの仮想 DOM では JavaScript で大部分の処理が行われますが、タグ付きのテンプレート リテラルでは、ブラウザの C++ で大部分の差分処理が行われます。

React または Preact で HTML タグ付きのテンプレート リテラルを初めて使用する場合は、htm ライブラリの使用が推奨されます。

Google Codelab のサイトやオンラインのコードエディタなどでは、タグ付きのテンプレート リテラル構文のハイライト表示は、基本的にサポートされませんが、一部の IDE やテキスト エディタではデフォルトでサポートされています(Atom や GitHub のコードブロック ハイライト表示ツールなど)。Lit チームでは、コミュニティと緊密に連携し、lit-plugin(Lit のプロジェクトに構文のハイライト表示、型チェック、入力支援を追加する VS Code のプラグイン)などのプロジェクトに取り組んでいます。

Lit と JSX、React DOM

JSX はブラウザでは実行されません。代わりに、プリプロセッサを使用して JSX を JavaScript 関数の呼び出しに変換します(通常は Babel 経由)。

たとえば、次のコードがあるとします。

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

このコードを Babel で変換すると次のようになります。

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

その後、React DOM で React の出力を受け取って実際の DOM(プロパティ、属性、イベント リスナーなど)に変換します。

lit-html では、トランスパイルやプリプロセッサなしにブラウザで実行できるタグ付きのテンプレート リテラルを使用します。つまり、Lit の使用にあたって必要となるのは HTML ファイル、ES モジュール スクリプト、サーバーのみです。たとえば以下のスクリプトのように、ブラウザですべて実行できるようになっています。

<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      import {html, render} from 'https://cdn.skypack.dev/lit';

      render(
        html`<div>Hello World!</div>`,
        document.querySelector('.root')
      )
    </script>
  </head>
  <body>
    <div class="root"></div>
  </body>
</html>

また、Lit のテンプレート システム(lit-html)では、従来の仮想 DOM ではなく DOM API を直接使用します。このため、React(2.8 KB)と react-dom(39.4 KB)のサイズが 40 KB となるのに対し、Lit 2 は 5 KB 未満で済みます(いずれも gzip 圧縮で軽量化した状態)。

イベント

React は合成イベント システムを採用しています。つまり、react-dom ではすべてのコンポーネントで使用するすべてのイベントを定義して、ノードの各タイプに相当する camelCase イベント リスナーを設定する必要があります。結果として、JSX にはカスタム イベント用のイベント リスナーを定義するメソッドがなく、デベロッパーは ref を使用して、強制的にリスナーを適用する必要があります。このように、React が考慮されていないライブラリを統合する場合は、React 用のラッパーを記述する必要が生じるため、効率的とは言えません。

一方、lit-html は DOM に直接アクセスしてネイティブ イベントを使用するため、イベント リスナーの追加も @event-name=${eventNameListener} のように簡単です。そのため、イベント リスナーの追加やイベントの発生に伴う実行時の解析も少なくて済みます。

コンポーネントとプロパティ

React コンポーネントとカスタム要素

LitElement では、カスタム要素を使ってコンポーネントをパッケージ化しています。コンポーネント化に関しては、カスタム要素と React コンポーネントとの間に一定の交換関係があります(状態とライフサイクルについて詳しくは、「状態とライフサイクル」のセクションをご覧ください)。

コンポーネント システムとしてのカスタム要素には、次のようなメリットがあります。

  • ブラウザ ネイティブなため、ツールが不要
  • innerHTMLdocument.createElement から querySelector まで、すべてのブラウザ API に適合
  • 通常はフレームワークをまたいで使用できる
  • customElements.define で登録を遅らせて DOM を「ハイドレート」できる

React コンポーネントとの比較では、カスタム要素には次のようなデメリットがあります。

  • クラスを定義せずにカスタム要素を作成できない(JSX のような関数コンポーネントがない)
  • 終了タグを含める必要がある
    • 注: 自己終了タグは、デベロッパーにとって便利な一方、多くのブラウザ ベンダーは否定的なため、新しい仕様では自己終了タグが採用されない傾向にあります。
  • DOM ツリーにノードが追加されると、レイアウトの問題が生じる可能性がある
  • JavaScript を介して登録する必要がある

Lit では、独自の要素システム上でカスタム要素を運用しています。これは、カスタム要素がブラウザに組み込まれていることと、クロスフレームワークのメリットがコンポーネントの抽象化レイヤーのメリットに勝ると考えられるためです。実際、lit-ssr 空間における Lit チームの取り組みにより、JavaScript の登録に関する主要な問題は解消されています。また、GitHub などの一部の企業では、カスタム要素の遅延登録を活用して、任意のスタイルでページのプログレッシブ エンハンスメントを行っています。

カスタム要素にデータを渡す

カスタム要素に対する最も多い誤解は、データを文字列でしか渡せない、という点です。このような誤解が生じるのは、要素の属性が文字列でしか記述できないことが理由と考えられます。確かに Lit では文字列属性が定義済みの型にキャストされますが、カスタム要素で複雑なデータをプロパティとして受け入れることは可能です。

たとえば、次のような LitElement 定義があるとします。

コード

// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('data-test')
class DataTest extends LitElement {
  @property({type: Number})
  num = 0;

  @property({attribute: false})
  data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}

  render() {
    return html`
      <div>num + 1 = ${this.num + 1}</div>
      <div>data.a = ${this.data.a}</div>
      <div>data.b = ${this.data.b}</div>
      <div>data.c = ${this.data.c}</div>`;
  }
}

このコードでは、プリミティブ型のリアクティブ プロパティ num を定義して、属性の文字列値を number に変換しています。そして複雑なデータ構造の場合は attribute:false を指定することで、Lit の属性処理を無効化します。

次に、このカスタム要素にデータを渡す方法を示します。

<head>
  <script type="module">
    import './data-test.js'; // loads element definition
    import {html} from './data-test.js';

    const el = document.querySelector('data-test');
    el.data = {
      a: 5,
      b: null,
      c: [html`<div>foo</div>`,html`<div>bar</div>`]
    };
  </script>
</head>
<body>
  <data-test num="5"></data-test>
</body>

状態とライフサイクル

その他の React ライフサイクル コールバック

static getDerivedStateFromProps

Lit では、プロパティと状態はどちらも同じクラスのプロパティのため、これに相当するものはありません

shouldComponentUpdate

  • 相当する Lit の要素は shouldUpdate
  • React と異なり、初回レンダリング時に呼び出される
  • React の shouldComponentUpdate と同様の働き

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate と類似する Lit の要素は updatewillUpdate です

willUpdate

  • update の前に呼び出される
  • getSnapshotBeforeUpdate と異なり、willUpdaterender の前に呼び出される
  • willUpdate でリアクティブ プロパティを変更しても、更新サイクルは再度トリガーされない
  • 他のプロパティによって変化し、残りの更新プロセスで使用されるプロパティ値を計算するのに適している
  • このメソッドは SSR によりサーバーで呼び出されるため、DOM にアクセスすることは推奨されない

update

  • willUpdate の後に呼び出される
  • getSnapshotBeforeUpdate と異なり、updaterender の前に呼び出される
  • update でリアクティブ プロパティを変更しても、その変更が super.update を呼び出すであれば、更新サイクルは再度トリガーされない
  • レンダリングした出力を DOM にコミットする前に、コンポーネント周辺の DOM から情報を取得するのに適している
  • このメソッドは SSR によりサーバーで呼び出されることはない

その他の Lit ライフサイクル コールバック

対応するものが React にないため、前のセクションで取り上げていないライフサイクル コールバックを以下に紹介します。

attributeChangedCallback

要素の observedAttributes のいずれかが変更されたときに呼び出されます。observedAttributesattributeChangedCallback はどちらもカスタム要素の仕様の一部で、Lit の内部で実装され、Lit 要素向けの属性 API を提供します。

adoptedCallback

コンポーネントが新しいドキュメントに移動(たとえば HTMLTemplateElementdocumentFragment からメインの document に移動)したときに呼び出されます。このコールバックもカスタム要素の仕様の一部ですが、コンポーネントによってドキュメントが変更されるような高度なユースケースでのみ使用してください。

その他のライフサイクルのメソッドとプロパティ

以下のメソッドとプロパティは、ライフサイクル プロセスの操作に役立つ、呼び出し、オーバーライド、待機が可能なクラスの構成要素です。

updateComplete

要素の更新が完了したときに、更新とレンダリングのライフサイクルの非同期を解決する Promise です。次に例を示します。

async nextButtonClicked() {
  this.step++;
  // Wait for the next "step" state to render
  await this.updateComplete;
  this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

updateComplete による解決時にオーバーライドしてカスタマイズする必要があるメソッドです。コンポーネントが子コンポーネントをレンダリングしていて、これらのレンダリング サイクルを同期する必要がある場合などに使用するのが一般的です。次に例を示します。

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

更新ライフサイクル コールバックを呼び出すメソッドです。通常は必要ありませんが、更新の同期やカスタムのスケジュール設定が必要なごく一部のユースケースで使用します。

hasUpdated

コンポーネントが 1 回以上更新されている場合、このプロパティは true になります。

isConnected

このプロパティはカスタム要素の仕様の一部で、要素がメインのドキュメント ツリーにアタッチされている場合は true になります。

Lit の更新ライフサイクルの可視化

更新ライフサイクルは次の 3 つに分けられます。

  • 更新前
  • 更新
  • 更新後

更新前

コールバック名付きのノードの有向非巡回グラフ。コンストラクタから requestUpdate、@property からプロパティ セッター、attributeChangedCallback からプロパティ セッター、プロパティ セッターから hasChanged、hasChanged から requestUpdate、そして requestUpdate から次の更新ライフサイクルのグラフにつながっています。

requestUpdate の後は、スケジュール設定された更新が待機しています。

更新

コールバック名付きのノードの有向非巡回グラフ。上の更新前ライフサイクルの図からの矢印は performUpdate につながっています。そして、performUpdate から shouldUpdate、shouldUpdate から「complete update if false(false なら更新を完了)」と willUpdate、willUpdate から update、update からレンダリングと次の更新後ライフサイクルのグラフ、またレンダリングからも次の更新後ライフサイクルのグラフにつながっています。

更新後

コールバック名付きのノードの有向非巡回グラフ。上の更新ライフサイクルの図からの矢印は firstUpdated につながっています。そして、firstUpdated から updated、updated から updateComplete につながっています。

フック

フックを使用するメリット

React のフックは、状態を必要とするシンプルな関数コンポーネントのユースケース向けに導入されたものです。多くの場合、関数コンポーネントとフックを使用すると、同等のクラス コンポーネントを使用するより、はるかにシンプルで読みやすいコードになります。しかしながら、状態の更新を非同期で行って、フックまたはエフェクト間でデータをやり取りする場合、一般的にフックパターンでは不十分で、リアクティブ コントローラなどのクラスベースのソリューションの方が適しています。

API リクエスト フックとコントローラ

API からデータをリクエストする場合、フックを記述するのが一般的です。たとえば、この React 関数コンポーネントでは次のような処理を行います。

  • index.tsx
    • テキストをレンダリングする
    • useAPI のレスポンスをレンダリングする
      • ユーザー ID とユーザー名
      • エラー メッセージ
        • (設計上)11 人目のユーザーに達したときの 404
        • API 取得を中止した場合の中止エラー
      • メッセージを読み込む
    • 操作ボタンをレンダリングする
      • 次のユーザー: 次のユーザーの API を取得する
      • キャンセル: API の取得を中止してエラーを表示する
  • useApi.tsx
    • useApi カスタムフックを定義する
    • API からユーザー オブジェクトを非同期で取得する
    • 次の情報を発行する
      • ユーザー名
      • 取得が進行中かどうか
      • エラー メッセージ
      • 取得を中止するためのコールバック
    • マウントが解除された場合に進行中の取得を中止する

詳しくは、「Lit とリアクティブ コントローラの実装」をご覧ください。

要点:

  • リアクティブ コントローラはカスタムフックとほぼ同じ働きをする
  • レンダリングできないデータをコールバックやエフェクト間でやり取りする
    • React では useRef を使用して useEffectuseCallback の間でデータをやり取りする
    • Lit ではプライベートなクラスのプロパティを使用する
    • React は基本的にプライベートなクラスのプロパティを模している

デフォルトのスロット

slot 属性が指定されていない HTML 要素は、デフォルトの無名スロットに割り当てられます。次の例では、MyApp で 1 つの段落を名前付きスロットに割り当てています。それ以外の段落は、デフォルトの無名スロットに割り当てられます。

Lit 環境

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot></slot>
        </div>
        <div>
          <slot name="custom-slot"></slot>
        </div>
      </section>
   `;
  }
}

@customElement("my-app")
export class MyApp extends LitElement {
  render() {
    return html`
      <my-element>
        <p slot="custom-slot">
          This paragraph will be placed in the custom-slot!
        </p>
        <p>
          This paragraph will be placed in the unnamed default slot!
        </p>
      </my-element>
   `;
  }
}

スロットの更新

スロットの子孫の構造が変更されると、slotchange イベントが発生します。Lit コンポーネントでは、イベント リスナーを slotchange イベントにバインドできます。次の例では、slotchange イベントの発生時に shadowRoot で検出された最初のスロットで assignedNodes をコンソールに記録しています。

@customElement("my-element")
export class MyElement extends LitElement {
  onSlotChange(e: Event) {
    const slot = this.shadowRoot.querySelector('slot');
    console.log(slot.assignedNodes({flatten: true}));
  }

  render() {
    return html`
      <section>
        <div>
          <slot @slotchange="{this.onSlotChange}"></slot>
        </div>
      </section>
   `;
  }
}

参照

参照の作成

Lit と React のいずれの場合も、render 関数が呼び出された後に HTML 要素への参照をエクスポーズします。なお、React と Lit でどのように DOM が作成されるのかを確認しておくことをおすすめします。DOM は、後で Lit の @query デコレータまたは React の参照を介して返されます。

React は、HTML 要素ではなく React コンポーネントを作成する関数パイプラインです。参照は HTML 要素をレンダリングする前に宣言されるため、メモリ内のスペースが割り当てられます。このため、参照の初期値は null となります(例: useRef(null))。これは、実際の DOM 要素はまだ作成(レンダリング)されていないためです。

ReactDOM では React コンポーネントを HTML 要素に変換した後、React コンポーネント内で ref という属性を探します。この属性が見つかった場合は、HTMLE 要素の参照を ref.current に設定します。

LitElement では、lit-html の html テンプレート タグ関数を使用して、内部で Template 要素を作成します。そして、レンダリング後にテンプレートのコンテンツをカスタム要素の Shadow DOM にスタンプします。Shadow DOM は、シャドウルートによってカプセル化されたスコープ付き DOM ツリーです。@query デコレータでは次にプロパティのゲッターを作成します。基本的にこのゲッターは、スコープされたルート上で this.shadowRoot.querySelector を実行します。

複数の要素のクエリ

次の例では、@queryAll デコレータを使ってシャドウルート内の 2 つの段落を NodeList として返しています。

@customElement("my-element")
export class MyElement extends LitElement {
  @queryAll('p')
  paragraphs!: NodeList;

  render() {
    return html`
      <p>Hello, world!</p>
      <p>How are you?</p>
   `;
  }
}

基本的に、@queryAllthis.shadowRoot.querySelectorAll() の結果を返す paragraphs のゲッターを作成します。JavaScript では、ゲッターで同じ目的を達成するよう宣言できます。

get paragraphs() {
  return this.renderRoot.querySelectorAll('p');
}

要素の変更のクエリ

@queryAsync デコレータは、別の要素のプロパティの状態に応じて変更可能なノードの処理に適しています。

次の例では、@queryAsync で最初の段落要素を検索しています。ただし、段落要素は renderParagraph によりランダムに奇数が生成された場合にのみレンダリングされます。@queryAsync ディレクティブは、最初の段落が使用可能な場合に解決する Promise を返します。

@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
  @queryAsync('p')
  paragraph!: Promise<HTMLElement>;

  renderParagraph() {
    const randomNumber = Math.floor(Math.random() * 10)
    if (randomNumber % 2 === 0) {
      return "";
    }

    return html`<p>This checkbox is checked!`
  }

  render() {
    return html`
      ${this.renderParagraph()}
   `;
  }
}

状態の仲介

React では、状態の仲介は React 自体により行われるため、コールバックを使用するのが一般的です。React では、要素で示される状態に依存しないようにします。DOM はレンダリング処理の結果にすぎません。

外部での状態管理

Lit と併せて Redux や MobX などの状態管理ライブラリを使用することができます。

Lit コンポーネントは、ブラウザのスコープ内で作成されます。このため、ブラウザのスコープ内に存在するライブラリはすべて Lit で使用できます。Lit の既存の状態管理システムを活用できるライブラリは数多くあります。

Lit コンポーネントで Redux を使用する方法については、Vaadin のこちらのシリーズをご覧ください。

大規模なサイトにおいて Lit で MobX を使用する方法については、Adobe の lit-mobx をご覧ください。

また、Web Components に GraphQL を組み込む方法については、Apollo Elements をご覧ください。

Lit はネイティブのブラウザ機能に対応しており、Lit コンポーネントではブラウザのスコープ内のほとんどの状態管理ソリューションを使用できます。

スタイル設定

Shadow DOM

Lit では、Shadow DOM を使用して、カスタム要素内でスタイルと DOM をネイティブにカプセル化します。シャドウルートは、メインのドキュメント ツリーとは別にシャドウツリーを生成します。つまり、ほとんどのスタイルはこのドキュメントにスコープされます。なお、色や他のフォント関連のスタイルなど、一部のスタイルはこのスコープから外れます。

Shadow DOM を使用すると、CSS の仕様に新しいコンセプトとセレクタを導入することもできます。

:host, :host(:hover), :host([hover]) {
  /* Styles the element in which the shadow root is attached to */
}

slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
  /*
   * Styles the elements projected into a slot element. NOTE: the spec only allows
   * styling the direcly slotted elements. Children of those elements are not stylable.
   */
}

スタイルの共有

Lit では、css テンプレート タグを介して CSSTemplateResults 形式のコンポーネント間でスタイルを簡単に共有できます。次に例を示します。

// typography.ts
export const body1 = css`
  .body1 {
    ...
  }
`;

// my-el.ts
import {body1} from './typography.ts';

@customElement('my-el')
class MyEl Extends {
  static get styles = [
    body1,
    css`/* local styles come after so they will override bod1 */`
  ]

  render() {
    return html`<div class="body1">...</div>`
  }
}

テーマ設定

シャドウルートを使用する場合、トップダウンのスタイルタグの手法が一般的な従来のテーマ設定では少し問題が生じます。Shadow DOM を使用した Web Components のテーマ設定を従来の方法で行うには、CSS カスタム プロパティを介してスタイル API をエクスポーズします。たとえば、マテリアル デザインで次のパターンを使用しているとします。

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

この場合、次のカスタム プロパティ値を適用してサイトのテーマを変更できます。

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

トップダウンのテーマ設定が必要不可欠で、スタイルをエクスポーズできない場合は、createRenderRoot をオーバーライドして this を返すようにすることで、いつでも Shadow DOM を無効にできます。こうして、カスタム要素にアタッチされたシャドウルートではなく、カスタム要素自体にコンポーネントのテンプレートをレンダリングします。この場合、スタイルのカプセル化、DOM のカプセル化、スロットは失われます。

製品化

IE 11

IE 11 などの古いブラウザに対応させる場合は、ポリフィル(約 33 KB)の読み込みが必要となります。詳しくはこちらをご覧ください。

条件付きバンドル

Lit チームでは、IE 11 用と最新のブラウザ用の 2 種類のバンドルを提供することを推奨しています。これには、次のようなメリットがあります。

  • ES 6 をより速く、ほとんどのクライアントに配信できる
  • トランスパイル済みの ES 5 はバンドルサイズが大幅に増える
  • 条件付きバンドルによって両方の環境を最大限に活用できる
    • IE 11 に対応できる
    • 最新のブラウザで読み込みが遅くならない

条件付きで配信するバンドルの作成方法について詳しくは、こちらのドキュメント サイトをご覧ください。