🐈

Lit(Web Component) はじめてみました。 LifeCycle編

2022/11/29に公開

こんにちは、エンジニア@milab です。

https://twitter.com/atmomo/status/1592517158160199680

弊社のCEOもツイートしていますが
Litを始めました。
ということで今回はLitについてまとめてみました!

なぜLitを取り入れたの?

エムアイ・ラボでは案件の規模や特性などに応じてAngular/React/Vue.jsを使い分けています。
そうすると、Angularで開発しながら「あれ、これこの間Reactのときにも作ったよね?」みたいなことが起こります。
最初はReactのComponentをAngularで流用したりして強引な再利用を行なっていたのですが
そもそも共通化できないか?という話になりました。

Web Componentを一から構築することも一瞬考えましたが
15分調べた時点でLitを使った方が圧倒的に早いと気付いてしまい
Litを使うことになりました。

Litって何?

Web Componentのライブラリです。

Web Componentって何?という方は以下の公式サイトがわかりやすいです。
https://developer.mozilla.org/ja/docs/Web/Web_Components

Web Componentに関してはSpeakerDeckでもわかりやすいスライドがUPされていたり
書籍もいくつか出ているのでぜひそちらをご参照ください!

Litは以下の公式サイトでさわってみることができます。
Hello World!
https://lit.dev/playground/

ライフサイクルまとめてみました!

使い方を調べてみたら英語でした。
でも、英語だからとあきらめるのはもったいない!
せっかくなのでライフサイクル(下記URL)を日本語にしながら
まとめてみようと思います。

https://lit.dev/docs/components/lifecycle/

更新前処理

No. Method or Property 概要
1 constructor 初期処理
2 connectedCallback ドキュメントのDOMにコンポーネントが追加されたときに呼び出されます。
3 adoptedCallback コンポーネントが新しいドキュメントに移動されたときに呼び出されます。
4 disconnectedCallback コンポーネントがドキュメントから削除されたときに呼び出されます
5 attributeChangedCallback コンポーネントの属性が変更されたときに呼び出されます。
6 someProperty.converter プロパティと属性を変換するためのカスタム コンバーター。 指定しない場合は、デフォルトの属性コンバーターを使用します。
7 someProperty.hasChanged 宣言されたすべてのプロパティには、プロパティが設定されるたびに呼び出される関数 hasChanged があります。 hasChanged が true を返す場合、更新がスケジュールされています。
8 requestUpdate hasChanged が true を返した場合、requestUpdate が起動し、更新が続行されます。
要素の更新を手動で開始するには、パラメーターを指定せずに requestUpdate を呼び出します。
プロパティ オプションをサポートするカスタム プロパティ セッターを実装するには、プロパティ名とその前の値をパラメーターとして渡します。
9 performUpdate 更新が実行されると、 performUpdate() メソッドが呼び出されます。 このメソッドは、他の多くのライフサイクル メソッドを呼び出します。
コンポーネントの更新中に通常は更新をトリガーする変更は、新しい更新をスケジュールしません。 これは、更新プロセス中にプロパティ値を計算できるようにするためです。 更新中に変更されたプロパティは changedProperties マップに反映されるため、後続のライフサイクル メソッドは変更に基づいて動作できます。
デフォルトでは、performUpdate は、ブラウザー イベント ループの次の実行の終了後にマイクロタスクとしてスケジュールされます。 performUpdate をスケジュールするには、super.performUpdate() を呼び出す前に何らかの状態を待機する非同期メソッドとして実装します。
10 shouldUpdate 更新を続行するかどうかを制御します。 shouldUpdate を実装して、更新を引き起こすプロパティの変更を指定します。 デフォルトでは、このメソッドは常に true を返します。
11 willUpdate update() の前に呼び出され、更新中に必要な値を計算します。
12 update プロパティ値を属性に反映し、render を呼び出して lit-html 経由で DOM をレンダリングします。 参考までにこちらに掲載。 このメソッドをオーバーライドしたり呼び出したりする必要はありません。 ただし、オーバーライドする場合は、必ず super.update(changedProperties) を呼び出してください。そうしないと、render は呼び出されません。
13 render lit-html を使用して要素テンプレートをレンダリングします。 LitElement 基本クラスを拡張するすべてのコンポーネントに render を実装する必要があります。
14 firstUpdated 要素の DOM が初めて更新された後、updated が呼び出される直前に呼び出されます。
要素のテンプレートが作成された後に 1 回限りの作業を実行するには、firstUpdated を実装します。
15 updated 要素の DOM が更新およびレンダリングされたときに呼び出されます。 更新後に何らかのタスクを実行するために実装します。
16 updateComplete 要素の更新が完了するとPromiseがレスポンス(resolve)されます。

サンプルソース

my-element.ts
import { html, LitElement, PropertyDeclaration, PropertyValues } from 'lit';
import { customElement, property } from 'lit/decorators';

@customElement('my-element')
export class Element extends LitElement {

  @property({
    attribute: 'prop',
    converter: (value) => {
      console.log(`prop.converter`, value);
      return value;
    },
    hasChanged: (value, oldvalue) => {
      console.log('prop,hadChanged', value, oldvalue)
      return (value !== oldvalue);
    }
  })
  private _props: string = '';

  constructor() {
    console.log('constructor');
    super();
  }

  override connectedCallback() {
    console.log('connectedCallback')
    super.connectedCallback();
  }

  adoptedCallback() {
    console.log('adoptedCallback');
  }  

  override attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
    console.log('attributeChangedCallback', name, _old, value);
    super.attributeChangedCallback(name, _old, value)
    this.requestUpdate(name, _old);
  }

  override requestUpdate(name?: PropertyKey | undefined, oldValue?: unknown, options?: PropertyDeclaration<unknown, unknown> | undefined): void {
    console.log('requestUpdate',name, oldValue, options);
    super.requestUpdate(name, oldValue, options);
  }

  override performUpdate(): void | Promise<void> {
    console.log('performUpdate');
    super.performUpdate();
  }

  shouldUpdate(changedProperties: PropertyValues): boolean {
     console.log('shouldUpdate', changedProperties)
    return true;
  }

  override willUpdate(changedProperties: PropertyValues<this>) {
    console.log('willUpdate', changedProperties);
    super.willUpdate(changedProperties);
  } 

  override update(changedProperties: PropertyValues): void {
    console.log('update', changedProperties);
    this.updateComplete.then((res) => {
      console.log('updateComplete', res);
    })
    return super.update(changedProperties);
  }

  override render() {
    console.log('render');
    return html`<div>${this._props}</div>`
  }

  override firstUpdated(changedProperties: PropertyValues): void {
    console.log('firstUpdated', changedProperties);
    return super.firstUpdated(changedProperties);
  }

  override updated(changedProperties:PropertyValues): void {
    console.log('updated', changedProperties);
  }

  override disconnectedCallback() {
    console.log('disconnectedCallback');
    super.disconnectedCallback();
  }

}

初期処理(propに'init'を設定)

No. Method or Property Values
1 更新前処理 constructor
2 requestUpdate undefined,undefined,undefined
3 attributeChangedCallback 'prop',null,'init'
4 prop.converter 'init'
5 requestUpdate 'prop',null,undefined
6 connectedCallback 初回の読込時のみコールされます。
7 更新処理 performUpdate
8 shouldUpdate Map(1) {'prop' => null}
9 willUpdate Map(1) {'prop' => null}
10 update Map(1) {'prop' => null}
11 render
12 更新後処理 firstUpdated Map(1) {'prop' => null} 初回の読込時のみコールされます。
13 updated Map(1) {'prop' => null}
14 updateComplete true

値を変更(propの'init'を'change'に変更)

No. Method or Property Values 備考
1 更新前処理 attributeChangedCallback 'prop','init','change'
2 prop.converter 'change'
3 requestUpdate 'prop','init',undefined
4 更新処理 performUpdate
5 shouldUpdate Map(1) {'prop' => 'init'}
6 willUpdate Map(1) {'prop' => 'init'}
7 update Map(1) {'prop' => 'init'}
8 render
9 更新後処理 updated Map(1) {'prop' => 'init'}
10 updateComplete true

コンポーネント削除

No. Method or Property Values 備考
1 後処理 disconnectedCallback

サンプルソースを作成しましたので、コンソールログで動作を確認してみてください。
chromeで動きます。

コンソールログで確認してみる!

デベロッパーツールのコンソールログで確認したときの表示をまとめてみました。
赤枠で囲っているところが変更した部分です。
※見やすさのため、デベロッパーツールの上部など消しています。

何もボタンを押下しないとき

初回の読み込み時なので、connectedCallbackとfirstUpdatedが呼ばれています。
_prop(以前セットされていたプロパティ)は未定義、
prop(現在セットされているプロパティ)にはnullが入っています。

initを押下したとき

初回ではないので、connectedCallbackとfirstUpdatedは呼ばれていません。
propに「init」がセットされました。

changeを押下したとき

_prop(以前セットされていたプロパティ)には「init」が、
prop(現在セットされているプロパティ)に「init」がセットされました。

removeを押下したとき

disconnectedCallbackメソッドのみが呼ばれました。

残課題

  • adoptedCallback の動作については未検証です。
  • someProperty.hasChangedがコールされない。

引き続き、深掘りして調査を進めたいと思います!

参考文献

https://lit.dev/docs/components/lifecycle/

https://developer.mozilla.org/ja/docs/Web/Web_Components

Discussion