この記事の概要
UIライブラリーをWeb Componentsで作って、ReactでもVueでもSvelteでも、色々な場所で使えたら嬉しくないですか?
私は結構夢がある話だと思っています。
というわけでその素振りをしてみました。
ユーザー規模的にReactを題材にしていますが大筋は他のフレームワークでも変わらないはずです。
Litとは?
Litとは、Web Componentsを構築するためのライブラリです。
通常のWeb Componentsは記述量が多いのと書き方が若干特殊に見えるので、Litが上手くラップして書きやすくしてくれます。
Lit環境(export側)の構築
まずはLit側の準備です。
公式のスターターキットがJavaScriptとTypeScript用それぞれ用意されているのですが、ちょろっと試すには重厚な印象でした。
というわけでViteから作成します。
$ yarn create vite
# Lit + TypeScriptの設定を選択
$ yarn
$ yarn dev
初期画面が出ました。
Storybookの導入
今回は軽いテストですが、コンポーネントの確認用にStorybookは用意しておきます。
Viteで作った環境なので、StorybookもあわせてビルダーをViteにします。
npx storybook init --builder @storybook/builder-vite
これで諸々の依存関係が追加されます。
yarn storybook
Storybookのinitをしたことがある人にはお馴染みのコンポーネント達が表示されました。
ただ、これで生成されるコードはイマイチです。
Shadow DOMやConstructed Stylesheetが適用されていなくてメリットが活かしきれていません。
分かりやすくsrc/stories
ごと削除してしまいます。
rm -rf src/stories
新しいコンポーネントの作成
とりあえずボタンを作り直してみます。
import { LitElement, css, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'
type Priority = 'primary' | 'secondary'
@customElement('my-button') // 1
export class MyButton extends LitElement {
@property() priority: Priority = 'primary' // 2
render() {
return html`
<button class=${this.priority}><slot></slot></button> // 3
`
}
static styles = css` // 4
button {
// 省略
}
.primary {
// 省略
}
.secondary {
// 省略
}
`
}
重要そうな場所をピックアップして説明します。
1. Decoratorでのカスタム要素の定義
@customElement
によってカスタム要素を定義しています。
Decoratorを使わない場合はcustomElements.define('my-button', MyButton);
の記載になります。
2&3. Decoratorでのプロパティの定義とslotでの要素の受け取り
このコードは、Reactで表現すると以下のような感じです。
const MyButton = ({ priority = 'primary' }: { priority: Priority }) => {
return (
<button className={priority}>{children}</button>
)
}
@property
は先ほど説明したのと似ています。
これを使わないと以下のようになります。
static properties = {
priority: {},
};
constructor() {
super();
this.priority = 'primary';
}
また、slot
によってchildren
っぽいことができます。
(厳密には違いますが、イメージとしてはそこまで相違ないと思っています。)
4. Constructed Stylesheetの生成
ハッシュによるスコープどころではない強力さです。
タグセレクターなどで指定されても上書きされないため、ほとんど無敵です。
補足
詳細は公式ドキュメントにも載っています。
Storybookでの確認
見た目が良い感じになりつつ、ちゃんとカスタム要素(my-button
)やslot
などが適用されています。
ビルド
設定は省略します。
設定を変えたい場合はvite.config.js
を触ればOKです。
yarn build
React環境(import側)の構築
こちらも、設定が簡単なのでViteで作ります。
$ yarn create vite
# React + TypeScriptの設定を選択
$ yarn
$ yarn dev
まずは立ち上がりました。
依存関係の追加とReact用コードに変換
Lit製のコンポーネントをReactに変換するためにインストールします
yarn add @lit-labs/react
新規にMyButton.tsx
を作成します。
ここのコードは割と見たままです。
import * as React from 'react'
import { createComponent } from '@lit-labs/react'
import { MyButton as MyButtonWC } from 'qiita-lit'
export const MyButton = createComponent({
tagName: 'my-button',
elementClass: MyButtonWC,
react: React,
})
変換したコンポーネントの使用
普通のReactと同様にimport { MyButton } from './MyButton'
からの<MyButton>
の使用でOKです。
コードとUIの両方が上手く見えていませんが、React側でも先ほどと同じようにWeb Componentsとして出力されています。
設定していたpriority
のプロパティを変えてもちゃんと反映されます。
React内でWeb Componentsを使おうとpropsやイベント周りが大変になるそうです。
それらがLitによって楽に扱えるようになるのは非常に嬉しいですね。
最後に
ある程度長く運用しているサービス群だと、色々なフレームワークを使っていることも多いのではないでしょうか。
サービスAはReact、サービスBはVueなど……。
どうしてもそういうのが起きがちな世界において、Web Componentsで基底ライブラリーを作っておければ乗り換えやリプレイスのコストも割と下げられる気がします。
そういった検討をする際にこの記事が役立っていれば幸いです。
最後まで読んでくださってありがとうございます!
Twitterでも情報を発信しているので、良かったらフォローお願いします!
Devトークでのお話してくださる方も募集中です!