Litのコード構成

Litは、WebComponents標準に準拠したクラスコンポーネントを実装するフレームワークです。

APIクライアントの実装パターン

APIクライアント用途の典型的な実装パターンは以下のような例になります。

import { html, LitElement } from "lit";
import { customElement、 state } from "lit/decorators.js";

@customElement("sample-app")
class SampleApp extends LitElement {
    @state()
    _person = {};

    async connectedCallback() {
        super.connectedCallback();
        this._person = await call_api();
    }
    
    render() {
      return html`<div> ${this._person.name} </div>`;
    }
}

APIクライアントの多くのケースで、コンポーネント呼び出し時にAPIレスポンスをセットアップします。

Litの場合、WebComponentsライフサイクルのconnectedCallback()フックを利用できます。
async connectedCallback()とするとAPIコールに伴うawaitを記述でき、処理フローが簡潔になります。

Reactの関数コンポーネントではuseEffect()を多用せざるを得ず、また非同期フローを(async() => {})()のIIFEでラップするといった仕様上の難点があり、構成が不明瞭になりがちでした。

ステート管理

Litのようにクラスコンポーネントの場合、ステートは単にメンバーとして定義できるため簡素です。関数コンポーネントのように特殊な操作関数を経由する必要はありません。

また、Litは@state()のようなデコレータを提供しており、ステート変数を宣言的に記述できます。React旧バージョンなど初期のクラスコンポーネントはthisにバインドするコードを必要としていたものが、デコレータで解消しています。

また初期化も可能であるため、不用意にundefinedにアクセスするエラーも低減できます。

なおデコレータはES候補でいずれJavascriptになりますが、現状はTypescriptと認識させてトランスパイルさせる方法が手軽です。
JavascriptはvalidなTypescriptであるため、この点以外は完全にJavascriptで書けます。

長いメソッドの切り出し

コンポーネントが複雑になるにつれ、class定義内部のコードが長大になります。
Javascriptのクラス機能により、メソッドを切り出せます。次の例はupdate()メソッドの2通りの定義を記述したものです。

import { html, LitElement } from "lit";
import { customElement、 state } from "lit/decorators.js";

@customElement("sample-app")
class SampleApp extends LitElement {
    @state()
    _counter = 0;

    update(_event) {
      this._counter = this._counter + 1;
    }
    
    render() {
      return html`<button @click=${this.update}>Trigger</button>`;
    }
}

// updateメソッドの別定義
SampleApp.prototype.update = function(_event) {
      this._counter = this._counter + 1;
}

Javascriptの仕様上、クラスのprototypeに定義した関数がメソッドになるため、メソッドはclassの外部に書けます。
thisを意図どおりにバインドするために、アロー関数は使えません。

Reduxとの接続

Reduxとの接続には、 pwa-helpersのconnect-mixinを使えます。

pwa-helpersはLitElement 1.x向けの古いライブラリですが、 connect-mixin.jsの実装は1ファイルであり、Litの mixinは単にJavascriptクラスのmixinパターンであるため、とくに問題なく動作します。

関係する部分のコード例は以下のようになります。connect()が拡張したクラスにはstateChanged(state)というコールバックが増えます。

import { connect } from 'pwa-helpers/connect-mixin.js';
import { store } from './store.js';

@customElement('sample-component')
class SampleComponent extends connect(store)(LitElement) {
    @state()
    _config = {};

    stateChanged(state) {
        this._config = state.config;
    }
}

stateChanged()は実装しだいで実行回数が非常に多くなります。
たとえば初回アクセスでデータが空の際にAPIコールする場合、API結果が反映されるまでの間にステートが変化することはよくあり、無数の重複APIコールが発生し得ます。

各API結果はReduxステートを変更するでしょうから、それがさらにstateChanged()をトリガーします。

stateChanged()内で何かの処理を一度だけ実行することは意外に困難です。ページリダイレクトも不安定な挙動になることがあります。

メモ化

Litは下層のテンプレートレイヤのdiff検出ロジックが効率的であるため、レンダリングのメモ化については考えることがありません。

ストア/コンポーネント間のデータ加工のレイヤについては、ReduxToolkitが内包している Reselectを用いてチューニングできます。
cahngeState(state)フックのstateをReduxコンポーネントのストアオブジェクトと同様に扱えます。

ReselectのcreateSelector()でデータ供給をメモ化するとstateChanged()の実行回数が減ります。

⁋ 2022/10/28↻ 2024/12/18
中馬崇尋
Chuma Takahiro