💎

Litで作ったコンポーネントをReactで使う方法

2023/12/05に公開

こんにちは、AIShift フロントエンドエンジニアの栗崎(@KK_sep_TT)です。
本記事はAIShift Advent Calendar 2023の 5 日目の記事になります。

Litとは

LitはWeb Componentをベースに作られたJSフレームワークです。LitはReactやVueと同じように複数のコンポーネントを組み立ててアプリケーションを作るフレームワークですが、LitではこのコンポーネントがWeb Componentで実装されています。Web Component で実装されていることにより、汎用性が上がったり、Shadow DOM の恩恵を受けてカプセル化されたCSSを書くことができます。

今回はそんなLitで作成されたコンポーネントをReactの中で使う方法についてまとめていきます。

https://lit.dev/

Lev.1 引数のない Web component 🌱

まずは引数のない非常にシンプルな例です。まずは以下のようなWeb Componentを用意します。cssで青色に装飾された "Hello World!"が出力されるコンポーネントです。

sample.ts
import { LitElement, css, html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("sample")
export class Sample extends LitElement {
  static styles = css`
    :host {
      color: blue;
    }
  `;

  render() {
    return html`<p>Hello, World!</p>`;
  }
}

このWeb ComponentをReactに取り込むためにはLitが提供するcreateComponent()関数を使用して以下のように実装します。

App.tsx
function App() {
  const WC = createComponent({
    tagName: "sample",
    elementClass: Sample,
    react: React,
  });

  return (
    <>
      <h1>React App</h1>
      <WC/>
    </>
  );
}

CreteComponent()関数にはtagName, elementClass, react (import した React)を引数として渡します。tagNameはWeb Componentのカスタムエレメントのタグ名になります。sampleに設定していますが、dev toolで要素を確認するとちゃんんと<sample>としてレンダリングされているのが確認できます。

ちなみにLitは他のフレームワークで簡単に動くことを得意としており、Litで作成したカスタムエレメントはそのまま使うことができます。例えばVueで動かしたければ、VueプロジェクトにLitをインストールしてVueのtemplate内に<lit-sample>のようにカスタムエレメントを書くだけで動きます。
以下の動画にLitで作成したコンポーネントをReact, Vueに取り込むデモがあります。

https://www.youtube.com/watch?v=QBa1_QQnRcs

Reactも単純なLitのコンポーネントであればCreateComponent()関数は必要ないのですが、ReactのHTML要素の扱いが特殊でそのままでは動かないケースがあり、Lit公式がラッパーを提供しているようです。

Lev.2 引数のある Web component 🌲

次に引数ありのWeb Componentについてです。といってもほとんどLev.1と変わらないのでサラッといきます。

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

@customElement("sample")
export class Sample1 extends LitElement {
  static styles = css`
    :host {
      color: blue;
    }
  `;

  @property()
  name?: string = "World";

  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}

Lit側はpropsとしてnameが追加されています。このnameを親から受け取って表示します。
続いて、React側です。通常のReactコンポーネントのpropsと同じようにWeb Componentに引数を渡すことができます。

App.tsx
function App() {
  const WC = createComponent({
    tagName: "sample",
    elementClass: Sample,
    react: React,
  });

  return (
    <>
      <h1>React App</h1>
      <WC name="Jack"/>
    </>
  );
}

Lev.3: イベントのある Web comoponent 🍄

最後に少し複雑なパターンとしてイベントのあるパターンです。その前にLitにおけるイベントとはなにかについて説明します。

Litにおけるイベント

イベントはLitにおいて変更を伝える標準的な方法です。これらの変更は通常ユーザとのインタラクションによって生じます。(例:ユーザがボタンをクリックしたときにclickイベントをディスパッチする)

以下のコードはボタンをクリックするとカウンタが増えるコードです。ボタンがクリックされたとき、clickイベントがディスパッチされます。

event.ts
export class MyElement extends LitElement {
  static properties = {
    count: {type: Number},
  };

  constructor() {
    super();
    this.count = 0;
  }
  render() {
    return html`
      <p><button @click="${this._increment}">Click Me!</button></p>
      <p>Click count: ${this.count}</p>
    `;
  }
  _increment(e) {
    this.count++;
  }
}
customElements.define('my-element', MyElement);

イベントを使った親と子のコミュニケーション

dispatchEvent()関数を使うとこのイベントを親コンポーネントに伝えることができます。子コンポーネントでnew Eventでカスタムイベントを作成してそのイベントをdispatchEvent()することで親コンポーネントにイベントを通知することができます。このようにして親子のやりとりが可能になります。

parent.ts
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";

import "./Child";

@customElement("lit-parent")
export class LitParent extends LitElement {
  handleClick() {
    console.log("parent handler");
  }

  render() {
    return html` <lit-child @button-click=${this.handleClick}></lit-child> `;
  }
}
child.ts
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";

@customElement("lit-child")
export class LitChild extends LitElement {
  buttonClick() {
    this.dispatchEvent(new Event("button-click", { composed: true }));
  }

  render() {
    return html`<p>Child</p>
      <button @click=${this.buttonClick}>button</button> `;
  }
}

Litでは以下の図のように子から親へはイベントで、親から子にはpropsでやりとりを行います。これはVueの "props down, event up" と同じですね。

親と子のeventのやりとりは以下の動画が非常に分かりやすいです。
https://www.youtube.com/watch?v=T9mxtnoy9Qw

Reactで使うとき

さて、ではイベントのあるLitコンポーネントをReactで使う方法です。以下のようなボタンがクリックされた時にイベントを親へディスパッチするLitコンポーネントを用意しました。

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

@customElement("sample")
export class Sample extends LitElement {
  static styles = css`
    :host {
      color: blue;
    }
  `;

  @property()
  name?: string = "World";

  onClick() {
    this.dispatchEvent(new Event("button-click", { composed: true }));
  }

  render() {
    return html`
      <p>Hello, ${this.name}!</p>
      <button @click=${this.onClick}>buton</button>
    `;
  }
}

このコンポーネントをReactで使うには以下のようにします。

App.tsx
function App() {
  const WC = createComponent({
    tagName: "sample",
    elementClass: Sample,
    react: React,
    events: {
      handleEvent: "button-click",
    },
  });

  return (
    <>
      <h1>React</h1>
      <WC
        name="jack"
        handleEvent={() => {
          console.log("event handled!");
        }}
      />
    </>
  );
}

export default App;

createComponent()関数に新たにeventsというプロパティが追加されました。ここにLitコンポーネントがディスパッチするイベントを書くことで、LitのイベントをReactのイベントハンドラーでキャッチすることができます。この例ではLitコンポーネントの中のボタンが押されるとhandleEventが呼ばれて"event handled!"の文字列がコンソール出力されます。

この手法を使えばReactで書いたロジックをLitコンポーネントに注入することができるので、結構使い道があるんじゃないかと思っています。

まとめ

  • Litで書いたコンポーネントは様々なフレームワークで動く
  • React で使う場合はLit公式のラッパーを使う
  • LitのプロパティはReactのPropsのように使える
  • eventを使うことでReactからLitへロジックの注入ができる

Litコンポーネントの汎用性は高く、他のフレームワークからも容易に使える事がわかったので、汎用性の高いコンポーネントはLitで作成するのは有用な選択肢な気がします。ただ、Litはクラスでコンポーネントを記述するので少し癖があります。イベントを使ってロジックを外部から注入することもできるので、UIだけLitで作ってReactなどで書いたロジックをイベント経由で注入して使うのもいいかもしれません。

AI Shift Tech Blog

Discussion