lit を使う機会があったので、「React でやる、あれをどうするのか」というメモを残しておく。
前提
環境構築めんどくさいのでplayground で試す。
state
Reactive properties という章がドキュメントにある通り、Lit には Reactive が備わっている。
class MyElement extends LitElement {
@property()
name: string;
}
このようにクラスフィールドに @property()
を付けるとそのフィールドが reactive になる。
これはクラスフィールドとしては public であるため、呼び出し元から書き換えることができてしまう。 つまり責務が閉じていない。
つまり、
const myElement = document.querySelector("my-element");
console.log(myElement.name); // world
myElement.name = "ojisan";
のようなコードを書けてしまう。これは React で言えば親が子のコンポーネントの状態を触れてしまうということで変に依存を生み、テストなどを考えるとあまり嬉しくない。
そこで lit ではこれの責務が閉じたモードも用意されている。これが React における state 相応のものだ。
そのためにはフィールドを protected
にした上で、@property()
ではなく @state()
を使う。
@state()
protected _active = false;
こうすれば protected なので継承しない限りは触れなくなる。
この説明では public property を悪のように書いたが、実 DOM を手続的に操作したいときは使いたい機能ではあるので、なんでもかんでも @state
を使えという主張はしない。
props
クラスフィールドで指定した値は親から受け取れる。
ただし @property
が必要である。
それは親から受け渡す以上は、親からそのフィールドが見えている必要があるからだ。
つまり @state
だと動かない。
import {html, css, LitElement} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
@property()
protected name = 'Somebody';
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
import {html, css, LitElement} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
@state()
protected name = 'Somebody';
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
ここでデータを渡す時の方法だが、普通に値を渡すとそれは文字列として渡されてしまう。 実際には JSON を渡したい、イベントハンドラを渡したいはずだ。もちろんそのようなユースケースにも対応できる。
それは渡すときの attribute に魔法の記号をつければ良い。
Expressionsにある通り、boolean なら ?
, property(要するに data) なら .
, event handler なら @
をつければ良い。
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("my-element")
class MyElement extends LitElement {
@property()
bodyText = "Text in child expression.";
@property()
label = "A label, for ARIA.";
@property()
editing = true;
@property()
value = 7;
render() {
return html`
<!-- Child expression -->
<div>Child expression: ${this.bodyText}</div>
<!-- attribute expression -->
<div aria-label=${this.label}>Attribute expression.</div>
<!-- Boolean attribute expression -->
<div>
Boolean attribute expression.
<input type="text" ?disabled=${!this.editing} />
</div>
<!-- property expression -->
<div>
Property expression.
<input type="number" .valueAsNumber=${this.value} />
</div>
<!-- event listener expression -->
<div>
Event listener expression.
<button @click="${this.clickHandler}">Click</button>
</div>
`;
}
clickHandler(e: Event) {
this.editing = !this.editing;
console.log(e.target);
}
}
イベントハンドラを渡すことができれば、親の状態更新を子が呼び出すといった react でよく見る dispatch を実現できるので覚えておこう。
lifecycle
React のライフサイクルと lit のそれは厳密には異なるが、考え方はほとんど同じなのでごちゃ混ぜにして説明する。厳密な仕様は公式ドキュメントを参考して欲しい。
FYI: https://lit.dev/docs/components/lifecycle
componentDidMount
React でいう componentDidMount は
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
@state()
protected name = 'Somebody';
connectedCallback() {
super.connectedCallback()
this.name = "ほげえ"
}
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
だ。こうすることで @state
が付いたものを初期化できる。
もしくは constructor を使っても良い。
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
@state()
protected name = 'Somebody';
constructor(){
super();
this.name = "aaa"
}
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
これは 「え、でもクラスフィールド宣言時に値を代入すれば良いだけじゃん」って思うかもしれない。 その意見は正しいが、decorator を使わない場合は class field には
class MyElement extends LitElement {
static properties = {
mode: { type: String },
data: { attribute: false },
};
}
しか現れないので、component の初期化する手順が必要となる。
それも「decorator 使えば良いじゃん」と思うかもしれないが、過去に decorator は仕様が巻き戻ったり、安定化するという確証を持てないので decorator を封じて開発したいモチベーションがあるかもしれない。実際 lit のドキュメントにも decorator を使わない場合の開発方法も指南 (例)されているので、decorator を使う前提を絶対とするのはやめたほうが良い。
componentDidUpdate
componentDidUpdate は React では コンポーネントが更新されたときに DOM を操作する API だ。これは親から渡される props が変わった時に、その値に応じて何かしらの処理をトリガーしたい時に使える。
componentDidUpdate(prevProps) {
// 典型的な使い方(props を比較することを忘れないでください)
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
このような処理は lit だと updated を使える。
updated(changedProperties) {
if (changedProperties.has('collapsed')) {
this._measureDOM();
}
}
この例だと、 changedProperties
は collapsed
を key にもつ Map であり、changedProperties.get('collapsed')
とすればアクセスできる。ただ、何が入ってくるは静的に知りたいので TypeScript で型を付けよう。その型は PropertyValueMap<T>
であり、T には 該当の Lit コンポーネントが入る。
class MyElement extends LitElement {
@property()
name: string;
updated(changedProperties: PropertyValueMap<MyElement>) {
if (changedProperties.has("name")) {
const prevName = changedProperties.get("name");
const currentName = this.name;
}
}
}
こうすると changedProperties には props の 型が MyElement のフィールドの型を伝って分かる。
おわりに
ひとまず state, props, lifecycle があれば React16.2 の DX で開発することができるので頑張ろう。