Web Componentsを使ってマイクロフロントエンドを実現する方法は多くのプロジェクトで採用されています。
私も仕事でWeb Componentsを扱っていますが、再利用性の部分で課題を感じており、その解決策をこのブログで提案したいと思います。
再利用性の課題感
例えば以下のようなコンポーネントを想像します
<h1>My Todo</h1>
<my-todos></my-todos>
このmy-todos
コンポーネントは、todo
の一覧を表示するコンポーネントで、JavaScriptのファイルを読み込むことで、どのサイトでも簡単にこのカスタムエレメントを使用することができます。また、適切にCSSを実装することで、レイアウトも調整できます。
ただ、これは本当に再利用性が高いと言えるのでしょうか?
例えば以下のようなユースケースではどうでしょうか
- リストスタイルのデザインではなく、カードタイプのデザインが必要な場合
- リストスタイルはそのままで良いが、個人のTodoではなく、TeamのTodoを表示したい場合
これらのニーズを満たすために、my-todosコンポーネントにプロパティを追加することを考えるかもしれません。
例えば、
<!-- カードスタイルのデザインということをプロパティで伝える -->
<my-todos design="card"></my-todos>
または、
<!-- teamのtodoということをプロパティで伝える -->
<my-todos type="team"></my-todos>
これらのアプローチでは、プロパティを増やすことでユースケースに対応しようとしますが、元々のシンプルな再利用性が失われてしまうことになります。
my-todos
という名前のコンポーネントで、チームのTodoを表示するという矛盾が生じてしまいます。
また、プロパティを追加することを避けて、ゼロから新しいコンポーネントを作る選択肢を取る場合、車輪の再発明が避けられず、開発が非効率になることが多いです。
解決策
そこで、今日紹介する解決策というのは、「slotにtodoを表示するレンダラーを注入する」というアプローチです。
この方法により、コンポーネント間の柔軟な切り替えが可能になり、再利用性を損なうことなく、異なるデザインやユースケースに対応できます。
コンポーネントの分割
まず、先ほどのmy-todos
コンポーネントをどう分割するかを考えます。
my-todos
コンポーネントの主な役割は「自分のTodoの一覧を取得して表示すること」です。この2つの機能を別々のコンポーネントに分けることで、役割ごとに責任を持たせることができます。
コンポーネント | 役割 |
---|---|
my-todos | 自分のtodoの一覧を取得し、親コンポーネントに伝える |
list-renderer | 材料(todoのid一覧)を受け取り、リストスタイルでtodoを描画する |
実装イメージ
Lit
で実装するイメージとしては以下のような感じです
親コンポーネント(呼び出し側)
親コンポーネントではイベントリスナーを設定し、my-todos
からtodoのIDの一覧を受け取り、それをlist-renderer
に渡します。
<my-todos @todo-fetched=${this.handleTodoFetched}>
<list-renderer .ids=${this.todoIds}></list-renderer>
</my-todos>
my-todos
コンポーネント
my-todos
コンポーネントは、fetchTodos
関数を使ってTodoの一覧を取得し、そのID一覧を親コンポーネントに渡します。
constructor() {
super()
fetchTodos().then((data) => {
const event = new CustomEvent('todo-fetched', {
detail: {
ids: data.ids,
},
})
this.dispatchEvent(event)
})
}
render() {
return html`
<div>
<slot></slot>
</div>
`
}
list-renderer
コンポーネント
受け取ったTodoのID一覧をもとに、リストスタイルでTodoを描画します。
@property({ type: Array })
ids = [] as number[]
render() {
return html`
${this.ids.map((id) => html` <todo-list .todoId=${id}></todo-list>`)}
`
}
メリット
このアプローチでは、list-renderer
を例えばcard-renderer
に置き換えるだけで、カードタイプのデザインに切り替えることができます。また、my-todos
をteam-todos
に置き換えることで、チームのTodoを表示することができます。
このように、コンポーネントを単一責任で分け、差異だけを変更することで、新しいユースケースに柔軟に対応できるようになります。
結論
Web Componentsの再利用性を高めるためには、コンポーネントの責務を分け、柔軟にカスタマイズできる仕組みを提供することが重要。
slot
とカスタムイベントを活用することで、再利用性を保ちながら、デザインやデータの変更にも簡単に対応できるようになります。
ソースコードなど
実装してみたサイト
ソースコード