React と Web Components の共存方法について
はじめに
UIコンポーネントをJavaScriptフレームワークに依存しない方法で一度定義し、他JavScriptフレームワーク間で再利用できるのが、Web Components です。
この記事は Web Components について深く掘り下げるものではありません。すべてのAPIとライフサイクルをカバーするわけではありませんし、shadowRoot
やslot
もカバーしません。
- Web Components への興味を喚起するのに簡潔な紹介
- React で実際に Webコンポーネント を使用するためのガイダンス
を紹介できたらと考えています。
要約
- Web Components はクラスで実装する
- React を使用してのデータバインディングや状態管理の機能はこれから
- Web Components と JavaScriptフレームワークの共存について
Web Components とは?
Web Componentsは、カプセル化された単一の責任を持つコードブロックです。あらゆるページで再利用することができます。
基本的には、JavaScript のクラスを定義し、それを HTMLElement から継承して、Web コンポーネントが持つあらゆるプロパティ、属性、スタイル、そしてもちろん、最終的にユーザーに表示するマークアップを定義します。
シンプルな Web Components を理解する
- Web Components を作るには、まず
HTMLElement
をextends
したJavaScriptのclass
を定義
class Example extends HTMLElement {
- 新しい Web Components(カスタム要素) を定義
if (!customElements.get("example-webcomponent")) {
customElements.define("example-webcomponent", Example);
}
- 描画する
<example-webcomponent />
connectedCallback
: Web コンポーネントが DOM に追加されたときに発火されるメソッド。
このメソッドを使用して、好きなコンテンツをレンダリングすることができます。
connectedCallback() {
this.innerHTML = "<div style='color: green'>This is Web Components</div>";
}
class Example extends HTMLElement {
connectedCallback() {
this.innerHTML = "<div style='color: green'>This is Web Components</div>";
}
}
if (!customElements.get("example")) {
customElements.define("example", Example);
}
React で Web Components を理解する
サンプルコードはこちらです。全体のコードを確認したい際には以下をご参照ください。
Web Components を理解するため、counter
コンポーネントを実装します。
主な実装内容として
- 状態変化を追加
- 表示される色を制御するために、
color
属性を追加。 - 表示される数値を制御するために、
increment
プロパティを追加。- 2、3、4 と値を増やせるようにします。
- 表示される色を制御するために、
color
属性を set します。属性に更新があったら、それに対応するために監視する属性を返すobservedAttributes
プロパティを追加。
static get observedAttributes() {
return ["color"];
}
observedAttributes
に set された属性が set, update されるたびに実行。
attributeChangedCallback(name) {
if (name === "color") {
this.update();
}
}
this.update()
を実装
update() {
this.valSpan.innerText = this._currentValue;
this.valSpan.style.color = this.getAttribute("color") || "black";
}
increment
プロパティを追加
increment = 1;
Web Components はできました。
では、React と組み合わせていきましょう
React と組み合わせる
まず、React が持つ Web Components の課題について
- React は HTML属性 にプリミティブ型しか渡せない(配列やオブジェクトは渡せない)
- どうしても配列として受け取りたい場合は
list="red, green, blue"
でthis.list = this.getAttribute("list").split(",") ?? [];
を処理する必要がありそうです。
- どうしても配列として受け取りたい場合は
- React は Web Component のイベントを渡せないので、手動で独自のハンドラを利用する必要がある
さらに React と Web Components は基本的に相互運用をサポートしていません。ですが、 Web Components への進捗を追いかけることはこちらできます。
面白いことに React の実験的ブランチで Web Components への対応されたと思っていたのですが、何か理由があってバージョン18にマージされなかったらしいのです。
現状の React と組み合わせる
React と融合させるには ref
を使用して、Web Component のインスタンスを取得し、値が変更されたときに手動でincrement
を set (wcRef.current.something = something
) することです。
import { useState, useRef, useEffect } from 'react';
import './counter';
export default function App() {
const [increment, setIncrement] = useState(1);
const [color, setColor] = useState('red');
const wcRef = useRef(null);
useEffect(() => {
wcRef.current.increment = increment;
}, [increment]);
return (
<div>
<div className="increment-container">
<button onClick={() => setIncrement(1)}>Increment by 1</button>
<button onClick={() => setIncrement(2)}>Increment by 2</button>
</div>
<select value={color} onChange={(e) => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<counter-webcomponent ref={wcRef} increment={increment} color={color}></counter-webcomponent>
</div>
);
}
下記のように Web Components と React API を各コンポーネントで対応させるよりも、
const wcRef = useRef(null);
useEffect(() => {
wcRef.current.increment = increment;
wcRef.current.decrease = decrease;
}, [increment, decrease]);
<counter-webcomponent ref={wcRef} increment={increment} decrease={decrease} color={color}></counter-webcomponent>
Web Components のタグ名(この場合はcounter-webcomponent
)をすべての属性とプロパティと一緒に渡して、
WebComponentWrapper
が Web Components(counter-webcomponent
) をレンダリングして
export default function App() {
const [increment, setIncrement] = useState(1);
const [color, setColor] = useState("red");
return (
<WebComponentWrapper
wcTag="counter-webcomponent"
increment={increment}
color={color}
/>
);
}
ref
を追加し、何がプロパティで、何が属性かを判断してくれるとよいではないでしょうか。
import { createElement, useRef, useLayoutEffect, memo } from 'react';
const _WebComponentWrapper = (props) => {
const { wcTag, children, ...restProps } = props;
const wcRef = useRef(null);
useLayoutEffect(() => {
const wc = wcRef.current;
for (const [key, value] of Object.entries(restProps)) {
if (key in wc) {
// プロパティであるか
if (wc[key] !== value) {
wc[key] = value;
}
} else {
// 属性(attribute)であるか
if (wc.getAttribute(key) !== value) {
wc.setAttribute(key, value);
}
}
}
});
return createElement(wcTag, { ref: wcRef });
};
export const WebComponentWrapper = memo(_WebComponentWrapper);
コンテンツがレンダリングされる前にこれらの更新を直ちに実行したいので、useLayoutEffect
を使用しています。useLayoutEffect
には依存関係の配列がないことにも注意してください。これは、頻繁に再レンダリングする傾向があるため、パフォーマンス観点でリスクがある場合があります。これを対応するために、React.memo
でラップしています。実際のプロパティが変更された場合にのみ再レンダリングを行い、それが起こったかどうかは単純な等価検査されます。
Web Components の未来
もしも、UIライブラリと Web Components を組み合わせるなら、Web Components はフォーカスやホバーされたボタンの見た目に、JSライブラリ、フレームワークはユーザーがそのボタンをクリックしたときに起こる処理(ロジックやデータ取得、ルーティング)に責務を分離させるのではないでしょうか。
Web Components を支えるツールlit-ssr、WebCも目立ち始めており、個人的にはワクワクしています。
Discussion