10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

この記事の概要

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を作成します。
ここのコードは割と見たままです。

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トークでのお話してくださる方も募集中です!

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?