リアクティブプロパティ

Litコンポーネントは(要素の属性やプロパティからの)入力を受け取ってステートをクラスフィールドもしくはプロパティに保存します。 リアクティブプロパティ(Reactive properties)は値が変更されるとリアクティブアップデートサイクルが発動され、コンポーネントが再レンダリングされます。 そして、オプションの設定によって要素の属性を読み書きすることが可能です。

class MyElement extends LitElement {
  @property()
  name: string;
}
class MyElement extends LitElement {
  static properties = {
    name: {},
  };
}

Litはリアクティブプロパティとそれに関連した要素の属性を取り扱います。

パブリックプロパティとインターナルステート

パブリックプロパティはコンポーネントのパブリックAPIの一部です。 一般的に、パブリックプロパティ、その中でもリアクティブプロパティは入力を扱います。

ユーザの入力に対応する以外でコンポーネントのパブリックプロパティを変更するべきではありません。 例えばメニューコンポーネントにselectedプロパティがあったとして、それは要素の属性として初期値を指定することができるとします。 ユーザが項目を選択した場合はコンポーネントがselectedプロパティを更新するべきです。 この場合、コンポーネントはイベントをでティスパッチ(dispatch)してコンポーネントの親コンポーネントにselectedプロパティが変更されたことを示す必要があるかもしれません。 詳しくはイベントをdispatchするを見てください。

Litにはインターナルリアクティブステート(internal reactive state)機能があります。 インターナルリアクティブステートはコンポーネントのAPIに含まれないリアクティブプロパティです。 このプロパティは対応する要素の属性を持ちません。通常、TypeScriptではprotectedもしはprivateにします。

@state()
private _counter = 0;
static properties = {
  _counter: {state: true};
};

constructor()
  super();
  this._counter = 0;
}

コンポーネントはインターナルリアクティブステートを扱います。 パブリックプロパティと同様にインターナルリアクティブステートを更新するとアップデートサイクルが発動します。 詳しくはインターナルリアクティブステートを見てください。

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

要素のリアクティブプロパティはデコレータもしくはstatic propertiesを使って宣言します。

いづれの場合も、オプションオブジェクトを渡すことでプロパティの動作を変更することができます。

デコレータでプロパティを設定する

下記のように@propertyデコレータをクラスフィールドの宣言に付与することでリアクティブプロパティを宣言します。

class MyElement extends LitElement {
  @property({type: String})
  mode: string;

  @property({attribute: false})
  data = {};
}

@propertyデコレータの引数はプロパティオプションです。 プロパティオプションを渡さないと全てのオプションのデフォルト値が適用されます。

static propertiesフィールドでプロパティを設定する

下記のようにstatic propertiesを使ってプロパティを設定します。

class MyElement extends LitElement {
  static properties = {
    mode: {type: String},
    data: {attribute: false},
  };

  constructor() {
    super();
    this.data = {};
  }
}

空のオプションオブジェクト({})が渡された場合はデフォルトのオプションが適用されます。

プロパティオプション

オプションオブジェクトに以下のプロパティを設定することができます。

attribute

プロパティに関連した属性を有効にするか、またはその属性名を変更したい場合はその属性名を渡します。 デフォルトはtrueです。 attributeをfalseにするとconverterreflecttypeオプションは無視されます。 詳しくはattributeオプションを見てください。

converter

プロパティと属性を相互に変換するためのカスタムプロパティコンバータを渡します。 渡されない場合はデフォルトプロパティコンバータを使います。

hasChanged

プロパティがセットされる毎に実行されます。更新を発動するか判定します。 デフォルトでは不等式(newValue !== oldValue)による判定を行います。 詳しくは変更判定の変更を見てください。

noAccessor

trueをセットするとデフォルトのプロパティアクセサを生成しません。 このオプションを使うことはほとんどないでしょう。 デフォルトはfalseです。 詳しくはnoAccessorオプションを見てください。

reflect

trueをセットするとプロパティの値をcustom elementのプロパティに対応する属性に反映します。 デフォルトはfalseです。 詳しくはreflectオプションを見てください。

state

trueをセットするとプロパティはインターナルリアクティブステートになります。 インターナルリアクティブステートはパブリックリアクティブプロパティのように更新を発動しますが、 Litはプロパティに対応する属性を生成しません。 そして、コンポーネント外からインターナルリアクティブステートのプロパティにアクセスするべきではありません。 このオプションは@stateデコレータと同じ効果を付与します。 デフォルトはfalseです。 詳しくはインターナルリアクティブステートを見てください。

type

文字列である属性をプロパティに変換する際に Litのデフォルトのコンバータはその文字列の値を指定された型(type)に変換します。 プロパティから属性に変換する場合は、その逆です。 converterオプションがセットされている場合、 このオプションの値はconverterオプションに渡されます。 セットされていない場合、デフォルトプロパティコンバータはStringに変換します。 詳しくはデフォルトプロパティコンバータを見てください。

TypeScriptを使う場合は、このオプションはフィールドの型と一致させる必要があります。 typeオプションはLitのランタイムではシリアライズとデシリアライズに使われます。 TypeScriptの型チェックと混同しないように注意してください。

オプションオブジェクトを指定しないもしくは空のオプションオブジェクトを指定することは、すべてのオプションにデフォルトの値を指定することと等価です。

インターナルリアクティブステート

インターナルリアクティブステートはコンポーネントのpublicなAPIではないリアクティブプロパティです。 このプロパティは対応する要素の属性を持ちません。 そして、コンポーネントの外側からアクセスされることを意図していません。 インターナルリアクティブステートはコンポーネントの内部でのみ使用されるべきです。

下記のように@stateデコレータを付与することによってインターナルリアクティブステートになります。

@state()
protected _active = false;

static propertiesクラスフィールドを使う場合は、プロパティオプションにstate: trueをセットするとインターナルリアクティブステートになります。

static properties = {
  _active: {state: true}
};

constructor() {
  this._active = false;
}

インターナルリアクティブステートはコンポーネントの外部から参照されるべきではありません。 TypeScriptではprivateもしくはprotectedを付けるべきです。 JavaScriptでは上記のようにprivateもしくはprotectedであるプロパティと認識できるように_をプロパティ名の先頭につけることを推奨します。

プロパティに関連した属性を持たないことを除いて、 インターナルリアクティブステートはパブリックリアクティブプロパティと同じ動作をします。 インターナルリアクティブステートに指定することができるプロパティオプションはhasChangedのみです。

@stateデコレータはminifierにプロパティ名が変更可能であるというヒントを与えます。

プロパティが変更されると何が起きるか

プロパティの変更はリアクティブアップデートサイクル(reactive update cycle)を発動します。 それはコンポーネントがテンプレートを再レンダリングすることを引き起こします。

プロパティが変更されると、下記の順番で処理が実行されます。

  1. プロパティのセッタが実行されます。
  2. プロパティのセッタがコンポーネントのrequestUpdateメソッドを実行します。
  3. プロパティの変更前の値と変更後の値を比較します。
    • デフォルトではnewValue !== oldValueのように比較します。
    • プロパティにhasChangedオプションがセットされている場合、 hasChanged関数はプロパティの変更前の値と変更後の値を引数にします。
  4. プロパティが変更されたと判定された場合、非同期的に更新がスケジュールされます。既に更新がスケジュールされていた場合はまとめて1回だけ更新が実行されます。
  5. コンポーネントのupdateメソッドが実行されます。(変更されたプロパティが属性に反映されます。コンポーネントのテンプレートが再レンダリングされます。)

プロパティの値がオブジェクトもしくは配列の場合、それ自体を置き換えないと更新が発動しません。 詳しくはプロパティでオブジェクトや配列を扱う際の注意点を見てください。

リアクティブアップデートサイクルのフックは多数あります。それらを変更することができます。 詳しくはリアクティブアップデートサイクルを見てください。

プロパティの変更判定の詳しい情報は変更判定の変更を見てください。

プロパティでオブジェクトや配列を扱う際の注意点

プロパティの値がオブジェクトもしくは配列の場合、その参照を変更しないと更新は発動しません。 プロパティの値がオブジェクトもしくは配列の場合、下記の2つの方法で操作することができます。

値の置き換える

オブジェクトや配列をイミュータブル(immutable)として扱います。 下記のように、myArrayから要素を削除する場合に新しい配列を作成します。

this.myArray = this.myArray.filter((_, i) => i !== indexToRemove);

この例ではシンプルなデータを扱っていますが、 複雑なオブジェクトを扱う場合はImmerのようなイミュータブルにデータを扱うためのライブラリを使うと可読性を保つことができるかもしれません。

手動で更新を発動する

下記のようにデータを変更して直接的に更新を発動するためにrequestUpdate()を実行します。

this.myArray.splice(indexToRemove, 1);
this.requestUpdate();

requestUpdate()が引数無しで実行されるとhasChanged()関数をスキップして更新がスケジュールされます。 requestUpdate()を実行したコンポーネントのみが更新されることに注意してください。 例えば、上記のコードではthis.myArrayを子コンポーネントのプロパティに渡すと参照が変わらないので変更を検知できません。 だから、子コンポーネントは更新されません。

一般的にほとんどのアプリケーションではイミュータブルオブジェクトをバケツリレーで受け渡すことが最善の方法です。 そうすることで必要なコンポーネントが確実に新しい値をレンダリングできるようになります。 (これによって、変更されたデータに依存しているコンポーネントのみが変更され、アプリケーション全体を更新するよりは効率的です。)

データを変更してrequestUpdate()を実行する方法は上級者向けです。 この方法では、 データを変更するすべてのコンポーネントを特定して、各コンポーネントでrequestUpdate()を実行する必要があります。 そうしないと、コンポーネントが期待通り更新されないかもしれません。 このようなコンポーネントがアプリケーションに広がっている場合、管理が大変です。

属性

JavaScriptのコードでコンポーネントへの入力として、 コンポーネントインスタンスのプロパティもしくはエクスプレッションを使うことで、 コンポーネントのプロパティにJavaScriptのデータをセットすることができます。 マークアップ内でコンポーネントへの入力として要素の属性に値をセットすることができます。 リアクティブプロパティに対してプロパティと属性の両方のインターファイスを提供することによって、 JavaScriptのコード内だけでなくサーバー側のウェブアプリケーションフレームワークのテンプレートが出力する静的なHTML等でLitコンポーネントを使用することを可能にします。 デフォルトでLitは各パブリックリアクティブプロパティに対応する属性を監視します。そして、属性が変更されるとそれに対応するプロパティが更新されます。 reflectオプションをセットするとプロパティが変更されると属性に反映されます。

要素のプロパティやコンポーネントのプロパティの場合は素のJavaScriptのデータがコンポーネントのプロパティに渡されますが、 要素の属性の場合は文字列がコンポーネントのプロパティに渡されます。 これは要素の属性とコンポーネントのプロパティ間の相互変換に影響を与えます。

attributeオプション

デフォルトでLitはすべてのパブリックリアクティブプロパティに対応する属性を作成します。 プロパティ名をすべて小文字にしたものが相互変換する属性名になります。

// 相互変換する属性名はmyvalueになります。
@property({ type: Number })
myValue = 0;
// 相互変換する属性名はmyvalueになります。
static properties = {
  myValue: { type: Number },
};

constructor() {
  super();
  this.myValue = 0;
}

attributeオプションに文字列を渡すと相互変換する属性名を違う名前にすることができます。

// 相互変換する属性名はmy-nameになります。
@property({ attribute: 'my-name' })
myName = 'Ogden';
// 相互変換する属性名はmy-nameになります。
static properties = {
  myName: { attribute: 'my-name' },
};

constructor() {
  super();
  this.myName = 'Ogden'
}

コンポーネントのプロパティと相互変換する要素の属性を作成しない場合はattributeオプションにfalseを指定します。 そうするとプロパティは属性の値によって初期化されません。そして、属性の値が変化してもプロパティの値は変化しません。

// プロパティと相互変換する属性を作成しません。
@property({ attribute: false })
myData = {};
// プロパティと相互変換する属性を作成しません。
static properties = {
  myData: { attribute: false },
};

constructor() {
  super();
  this.myData = {};
}

インターナルリアクティブステートは要素の属性の影響をまったく受けません。

下記のようにマークアップで相互変換する属性に値をセットすることによってコンポーネントのプロパティの初期値をセットすることができます。

<my-element myvalue="99"></my-element>

デフォルトプロパティコンバータ

LitのデフォルトプロパティコンバータはStringNumberBooleanArrayObjectをプロパティの型として取り扱います。

デフォルトプロパティコンバータを使うには、プロパティにtypeプロパティオプションをセットします。

// デフォルトプロパティコンバータを使います。
@property({ type: Number })
count = 0;
// デフォルトプロパティコンバータを使います。
static properties = {
  count: { type: Number },
};

constructor() {
  super();
  this.count = 0;
}

デフォルトプロパティコンバータもしくはカスタムプロパティコンバータをプロパティにセットしない場合、 デフォルトでtype: Stringがセットされます。

各デフォルトプロパティコンバータの動作を下記の表で説明します。

要素の属性からコンポーネントのプロパティへ

変換
String 要素に対応する属性があると、プロパティにその属性の値をセットします。
Number 要素に対応する属性があると、プロパティにNumber(attributeValue)をセットします。
Boolean 要素に対応する属性があると、プロパティにtrueをセットします。
そうでない場合、プロパティにfalseをセットします。
Object, Array 要素に対応する属性があると、プロパティにJSON.parse(attributeValue)をセットします。

Boolean以外の場合で 要素に対応する属性がない場合、 プロパティはデフォルトの値もしくはデフォルトの値がセットされていない場合はundefinedになります。

コンポーネントのプロパティから要素の属性へ

変換
String, Number プロパティがnullもしくはundefinedでない場合、属性にプロパティの値をセットします。
プロパティの値がnullもしくはundefinedの場合、要素から属性を削除します。
Boolean プロパティの値がtrueになる値の場合、要素に空の属性を作成します。
プロパティの値がfalseに値の場合、要素から属性を削除します。
Object, Array プロパティがnullもしくはundefinedでない場合、属性にJSON.stringify(propertyValue)をセットします。
プロパティの値がnullもしくはundefinedの場合、要素から属性を削除します。

カスタムプロパティコンバータ

カスタムプロパティコンバータはプロパティの宣言時にプロパティオプションのconverterオプションでセットすることができます。

myProp: {
  converter: // カスタムプロパティコンバータ
}

converterオプションにはオブジェクト(object)もしくは関数をセットすることができます。 オブジェクトをセットする場合、下記のようにfromAttributetoAttributeを設定することができます。

prop1: {
  converter: {
    fromAttribute: (value, type) => {
      // `value`は文字列です。
      // それを`type`型に変換して返します。
    },
    toAttribute: (value, type) => {
      // `value` は`type`型です。
      // それを文字列に変換して返します。
    }
  }
}

converterが関数の場合、その関数は上記のfromAttributeの役割を行います。

myProp: {
  converter: (value, type) => {
      // `value`は文字列です。
      // それを`type`型に変換して返します。
  }
}

toAttributeをセットされていないプロパティは、デフォルトのコンバータが適用されます。

toAttributenullもしくはundefinedを返すと属性が削除されます。

reflectオプション

コンポーネントのプロパティが変更されると、それに対応する要素の属性に反映できるように設定することができます。 反映された要素の属性はCSSセレクタに使うことができるので便利です。

// nameプロパティの値は属性に反映されます。
name: {reflect: true}

プロパティが変更されると、 Litはデフォルトプロパティコンバータもしくはカスタムプロパティコンバータを使ってプロパティの値を変換します。その値を属性にセットします。

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

@customElement('my-element')
class MyElement extends LitElement {
  @property({type: Boolean, reflect: true})
  active: boolean = false;

  static styles = css`
    :host {
      display: inline-block;
    }

    :host([active]) {
      border: 1px solid red;
    }`;

  render() {
    return html`
      <span>Active: ${this.active}</span>
      <button @click="${() => this.active = !this.active}">Toggle active</button>
    `;
  }
}

一般的に属性は要素への外部からの入力とみなされます。だから、要素の内部に保持されているプロパティの値を属性に反映する際は注意する必要があります。 今はスタイルやアクセシビリティのためにそうすることが必要です。 将来、:state pseudo selectorAccessibility Object Modelがサポートされれば、そのために属性を反映する必要はなくなります。

大きいオブジェクトのシリアライズはパフォーマンスの低下をもたらす原因になるので、 型がオブジェクトもしくは配列のプロパティを属性に反映することはお勧めしません。

更新している間、Litはステートの反映を確認します。 プロパティの変更が属性に反映されて、属性の変更がプロパティを更新することで無限ループが発生する可能性があると不安に思うかもしれません。 これを防ぐために、Litはプロパティや属性がセットされると確認します。

カスタムプロパティアクセサ

デフォルトでLitElementはすべてのリアクティブプロパティに対してセッタ/ゲッタのペアを生成します。 プロパティをセットする毎にセッタが実行されます。

// プロパティの宣言
@property()
greeting: string = 'Hello';
...
// 後にプロパティをセットします。
this.greeting = 'Hola'; // 生成されたgreetingのプロパティアクセサを実行します。
// プロパティの宣言
static properties = {
  greeting: {},
}
constructor() {
  this.super();
  this.greeting = 'Hello';
}
...
// 後にプロパティをセットします。
this.greeting = 'Hola'; // 生成されたgreetingのプロパティアクセサを実行します。

生成されたアクセサは自動的にrequestUpdate()を実行します。 そして、更新中でない場合、更新を開始します。

カスタムプロパティアクセサを作成する

下記のようにプロパティのゲッタとセッタを変更することで、プロパティを取得する処理やセットする処理を変更することができます。

private _prop = 0;

@property()
set prop(val: number) {
  this._prop = Math.floor(val);
}

get prop() { return this._prop; }
static properties = {
  prop: {},
};

_prop = 0;

set prop(val) {
  this._prop = Math.floor(val);
}

get prop() { return this._prop; }

上記のように@propertyもしくは@stateデコレータをセッタに付与してカスタムプロパティアクセサを作成します。 @propertyもしくは@stateが付与されたセッタはrequestUpdate()を実行します。

ほとんどの場合、カスタムプロパティアクセサを設定する必要はありません。 変更後のプロパティを使った処理を行う場合はwillUpdateコールバックを使うことを推奨します。この方法ではアップデートサイクル中にプロパティの値を変更したとしても、新たな更新は発動しません。 要素が更新された後に実行される処理を変更したい場合はupdatedコールバックを使うことを推奨します。

クラスでプロパティに対するアクセサが定義されている場合、Litはそれらをデフォルトのアクセサで上書きしません。 クラスでプロパティを定義してそのプロパティに対するアクセサが定義されていない場合、スーパークラスでプロパティとアクセサが定義されていてもLitはデフォルトのアクセサを使います。

noAccessorオプション

クラスでプロパティを定義してそのプロパティに対するアクセサが定義されていない場合、スーパークラスでプロパティとアクセサが定義されていてもLitはデフォルトのアクセサを使います。 この場合でスーパークラスで定義されたアクセサを使うにはnoAccessortrueをセットします。

static properties = {
  myProp: { type: Number, noAccessor: true }
};

クラスでアクセサを定義している場合、noAccessorをセットする必要はありません。

変更判定の変更

リアクティブプロパティに値をセットするとhasChanged()が実行されます。

hasChangedはプロパティの1つ前の値と現在の値を比較します。そして、プロパティが変更されたかどうか判定します。 hasChanged()がtrueを返すと、更新が既にスケジュールされていない場合、Litは要素の更新を開始します。 更新に関する詳しい情報はリアクティブアップデートサイクルを見てください。

hasChanged()のデフォルトの実装はnewVal !== oldValです。

下記のようにプロパティオプションに関数をセットすることでhasChanged()を変更することができます。

@property({
  hasChanged(newVal: string, oldVal: string) {
    return newVal?.toLowerCase() !== oldVal?.toLowerCase();
  }
})
myProp: string | undefined;
static properties = {
  myProp: {
    hasChanged(newVal, oldVal) {
      return newVal?.toLowerCase() !== oldVal?.toLowerCase();
    }
  }
};

下記の例ではhasChanged()は奇数の場合のみtrueを返します。

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

@customElement('my-element')
class MyElement extends LitElement {
  @property({
    // newValが奇数の場合のみ更新されます。
    hasChanged(newVal: number, oldVal: number) {
      const hasChanged: boolean = newVal % 2 == 1;
      console.log(`${newVal}, ${oldVal}, ${hasChanged}`);
      return hasChanged;
    },
  })
  value: number = 1;

  render() {
    return html`
      <p>${this.value}</p>
      <button @click="${this.getNewVal}">Get new value</button>
    `;
  }

  getNewVal() {
    this.value = Math.floor(Math.random() * 100);
  }
}

License

Japanese part

Creative Commons Attribution-NonCommercial 4.0 International Public License

Copyright (c) 2022 38elements

Other

Creative Commons Attribution 3.0 Unported

Copyright (c) 2020 Google LLC. All rights reserved.

BSD 3-Clause License

Copyright (c) 2020 Google LLC. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.