ReactとWeb Componentsを組み合わせる
これまでの連載で学んできた通り、Web Componentsはカスタム要素という形で、組み込みのHTML要素のように振る舞うことができます。Reactは、JSXというHTMLライクな記法の中に、HTML要素を書くことができます。それなら、リスト2のように、Web ComponentsをReactの中で使うこともできるのではないでしょうか。
import React from 'react'; import './my-custom-element'; // Web Componentsを読み込む export default function App() { return ( <div> <my-custom-element></my-custom-element> </div> ); }
リスト2のようなシンプルな例であれば、動作する可能性は高いです。しかし、Props(属性)を渡し始めたり、カスタム要素の中で発生したイベントのコールバックをReactで受け取ろうとすると、うまくいかなくなります。これは、React DOMで管理しているのはDOMツリーまでであり、Shadow DOMとの連携については、まだサポートできていないのが原因です。
カスタム要素の属性をReact側の値と紐付ける方法については、React 19でのサポートを目指した取り組みがありますので、将来に期待しましょう。
@lit/react
実は、第3回と第4回で解説したLitというライブラリには、Reactとの連携をサポートするライブラリがあります。それが、@lit/reactです。このライブラリを使うと、ReactとWeb Componentsを比較的容易に組み合わせることができます。
NPM管理下のプロジェクトであれば、リスト3のようにしてインストールできます。
$ npm install lit @lit/react
おそらく lit
は無くても動きますが、今回のサンプルの中ではLitを利用してカスタム要素を作っているので、一緒にインストールしておきます。
これで @lit/react
を試す準備ができました。
Reactコンポーネントを作る
さて、まずは属性を持たないプレーンなカスタム要素をReactコンポーネントとして使えるようにしてみましょう。リスト4のカスタム要素を対象にします。
import {LitElement, html} from 'lit'; export class MyCounterElement extends LitElement { static properties = { count: { type: Number, reflect: true } } constructor() { super(); this.count = 0; } render() { return html^^ <p>カウント: ${this.count}</p> <button @click="${() => this.increment()}">+1</button> ^^; } increment() { this.count += 1; } } customElements.define('my-counter-element', MyCounterElement);
ボタンを押すとカウントアップするだけの、シンプルなカスタム要素です。このカスタム要素をReactコンポーネントとして使えるようにするには、リスト5のようにします。
import React from 'react'; import { createComponent } from '@lit/react'; import { MyCounterElement } from './my-counter-element'; export const MyCounterElementComponent = createComponent({ // (1) tagName: 'my-counter-element', elementClass: MyCounterElement, react: React, })
(1)の createComponent
は、カスタム要素をReactコンポーネントとして使えるようにするための関数です。リスト5の例では、次の3つのパラメータを渡しています。
-
tagName
:カスタム要素のタグ名 -
elementClass
:カスタム要素のクラス -
react
:Reactライブラリ
react
が少しわかりづらいので補足しますと、Reactにはpreactのような互換ライブラリが存在しており、@lit/react
は互換ライブラリ向けにコンポーネントを作成することもできるのです。今回はReactを使うので、React
を渡しています。
さて、リスト5で生み出したReactコンポーネントの MyCounterElementComponent
を、実際に使ってみましょう。サンプルコードの src/App.jsx
に配置してみました(リスト6)。
import { MyCounterElementComponent } from './simple-counter/MyCounterElementComponent'; import './App.css' function App() { return ( <> <h1>ReactからWeb Componentsを使う</h1> <h2>カスタム要素をコンポーネントとして配置する</h2> <MyCounterElementComponent />{/ (1) /} </> ) } export default App
(1)のように、Reactコンポーネントとして配置しました。ブラウザで http://localhost:5173/
を開くと、図3のように表示されます。
まるで普通のReactコンポーネントのようですね。ただ、ここまでなら直接 <my-counter-element>
を配置した場合にも、同じ挙動になりますので、まだ @lit/react
の恩恵を受けているとは言い難いです。
Reactからプロパティを流し込む
次に、Reactからpropsの形でプロパティを流し込んでみましょう。リスト7のように、名前を表示するだけの簡単なサンプルを用意しました。
import {LitElement, html} from 'lit'; export class MyNamePropertyElement extends LitElement { static properties = { person: {}, } constructor() { super(); this.person = { name: "名無し" }; } render() { return html^^ <p>こんにちは、${this.person.name}さん!</p> ^^; } } customElements.define('my-name-property-element', MyNamePropertyElement);
ここで注目したいのが、プロパティとして定義した person
に渡すのが、オブジェクトである、という点です。渡すのが文字列であれば、カスタム要素を直接書いても問題なく動くのですが、オブジェクト参照を渡すとなると、ReactとWeb Componentsの連携においては苦手な分野です。つまり、@lit/react
がその苦手な分野をうまく吸収してくれるのか、という点が今回のポイントです。
では、リスト8のように、@lit/react
を使ってReactコンポーネントを作成して、動かしてみましょう。
import React from 'react'; import { createComponent } from '@lit/react'; import { MyNamePropertyElement } from './name-properties-element'; const MyNamePropertyElementComponent = createComponent({ // (1) tagName: 'my-name-property-element', elementClass: MyNamePropertyElement, react: React, }) export const LitPropertiesAsReactProps = () => { return ( <MyNamePropertyElementComponent person={{ name: 'リアクト' }} />{/ (2) /} ) };
(1)の createComponent
の設定内容は、リスト5と同じです。(2)では、person
というプロパティに、オブジェクトを渡しています。ブラウザで http://localhost:5173/
を開くと、図4のように表示されます。
うまく表示されましたね。では、カスタム要素を直接書いた場合はどうなるのでしょうか。リスト9のように、returnの中を書き換えてみます。
// (略) return ( <my-name-property-element person={{ name: 'リアクト' }} /> ) // (略)
ブラウザで http://localhost:5173/
を開くと、図5のように表示されます。
名前が表示されませんでした。カスタム要素の内部でログを取ってみると、person
には文字列の [object Object]
が入っており、オブジェクトを渡すことには失敗しています。ここを上手くやってくれるのが、@lit/react
です。
まとめ
今回は、ReactとWeb Componentsの連携方法について、特にプロパティの話題を解説しました。次回は、コールバックイベントの取り扱いや、childrenとslotの関係について解説します。