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()
の実行回数が減ります。
Chuma Takahiro