2023年12月18日月曜日

ReactのWebComponentsでslotされたものを使い回す

この記事は Uzabase Advent Calendar 2023の18日目 の記事です。


はじめに

UzabaseのProduct Divisionではプロダクトのフロントエンドコンポーネントを他のプロダクトでも再利用できるようWebComponentsを利用しています。WebComponentsにはスロットという機能があり、slotタグによって外部から要素を渡すことができます。

たとえば、ローディングスピナーを表示するspinner-slotという自作要素をlitで作ってみます。2つ上下にローディングスピナーは出ますが、下の方はslotによって外部から差し込まれたスピナーに置き換わっています。

次のように表示されます。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>spinner slot</title>
<script type="module" src="/index.js"></script>
<style>
.another-spinner {
width: 32px;
height: 32px;
margin: 10px auto;
border: 4px #ddd solid;
border-top: 4px red solid;
border-radius: 50%;
animation: another-spinner-anime .45s infinite linear;
}
@keyframes another-spinner-anime {
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body style="display: flex; justify-content: center;">
<div>
<spinner-slot><!-- <div class="another-spinner"></div> --></spinner-slot>
<spinner-slot><div class="another-spinner"></div></spinner-slot>
</div>
</body>
</html>
view raw index.html hosted with ❤ by GitHub
import {LitElement, css, html} from 'lit-element';
class MyElement extends LitElement {
static styles = css`
.spinner {
width: 32px;
height: 32px;
margin: 10px auto;
border: 4px #ddd solid;
border-top: 4px blue solid;
border-radius: 50%;
animation: spinner-anime 1.0s infinite linear;
}
@keyframes spinner-anime {
100% {
transform: rotate(360deg);
}
}
`;
render() {
return html`
<div>
<slot>
<div class="spinner"></div>
</slot>
</div>
`;
}
}
customElements.define('spinner-slot', MyElement);
view raw index.js hosted with ❤ by GitHub


業務の中で、このスロット機能を利用して複数の場所で差し込まれた要素を使おうとしました。今回は名前付きslotを利用して、spinner-slotの中で2つの名前付きslot(another-icon)を表示するようにしてみます。

次のように表示されます。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>spinner slot</title>
<script type="module" src="/index.js"></script>
<style>
.another-spinner {
width: 32px;
height: 32px;
margin: 10px auto;
border: 4px #ddd solid;
border-top: 4px red solid;
border-radius: 50%;
animation: another-spinner-anime .45s infinite linear;
}
@keyframes another-spinner-anime {
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body style="display: flex; justify-content: center;">
<div>
<spinner-slot><!-- <div class="another-spinner"></div> --></spinner-slot>
<spinner-slot><div slot="another-icon" class="another-spinner"></div></spinner-slot>
</div>
</body>
</html>
view raw index.html hosted with ❤ by GitHub
import {LitElement, css, html} from 'lit-element';
class MyElement extends LitElement {
static styles = css`
.spinner {
width: 32px;
height: 32px;
margin: 10px auto;
border: 4px #ddd solid;
border-top: 4px blue solid;
border-radius: 50%;
animation: spinner-anime 1.0s infinite linear;
}
@keyframes spinner-anime {
100% {
transform: rotate(360deg);
}
}
`;
render() {
return html`
<div>
<slot name="another-icon">
<div class="spinner"></div>
</slot>
<slot name="another-icon">
<div class="spinner"></div>
<slot>
</div>
`;
}
}
customElements.define('spinner-slot', MyElement);
view raw index.js hosted with ❤ by GitHub

あれれ、これはうまくいきません。どうやら子要素は1つのslotにしかアサインされないようです。ということは、another-icon-1のように数字をつけたりしてslot nameを変えて欲しいだけの要素を差し込むしかないみたいです。同じものを出したいだけなのにスッキリしないですよね。なので、どうにかして1つの要素を差し込むだけでOKにしてみます。


これが完成形だ!?

業務ではReactを使ってWebComponentを作っていたので、今回はReactで表現してみます。

サンプルコード(github: react-slot)

isEvenという名前の要素を1つ差し込むだけで、slotにアサインされている要素をuseRefで参照しています。

Templateコンポーネントの中で、参照した要素をcloneNodeでコピーして表示するようにしました。

ただ、この方法でやりたいことはできましたが、描画の最初のほうでアサインされた元のslotが一瞬見えてしまう課題がでてきました。


おわりに

今回は1つの要素を差し込むだけで複数のslotに表示させてみました。ただし、課題も残っています。スクリプトをロードするまで描画しないようにしたら解消できるかもしれません。解決した方はコメントをお願いします。

ちなみに、atomicoというフレームワークで用意されているuseSlotを参考に作りました。そちらでは自前で実装する必要はなさそうです。atomicoもぜひご覧ください。