はじめに
以前slotを何度も利用したいという記事を書きました
ここではslotの名前を変えて対処を行いましたが、1つのslotを使い回す方法がわかったのでまとめます
やり方
まずはコードをすべて載せます
import { FC, useEffect, useRef, useState } from "react";
import { createRoot, Root } from "react-dom/client";
import { CacheProvider, css, EmotionCache } from "@emotion/react";
import createCache from "@emotion/cache";
import resetCss from "./reset.css?inline";
const useSlot = (ref: React.RefObject<HTMLSlotElement>) => {
const [childNodes, setChildNodes] = useState<ChildNode[]>();
useEffect(() => {
const { current } = ref;
if (!current) return;
setChildNodes(current.assignedNodes() as ChildNode[]);
}, []);
return childNodes;
};
const SampleElement: FC = () => {
const slot = useRef<HTMLSlotElement>(null);
const result = useSlot(slot);
if (result && result.length > 0) {
const [children] = result;
return (
<>
{[...Array(5)].map((_, n) => (
<div key={n}>
<span ref={(ref) => ref?.appendChild(children.cloneNode(true))}>
{n}
</span>
</div>
))}
</>
);
}
return (
<>
<slot name="test" ref={slot} style={{ display: "none" }} />
<div>no children</div>
</>
);
};
class SampleElementComponent extends HTMLElement {
root: Root;
cache: EmotionCache;
constructor() {
super();
this.attachShadow({ mode: "open" });
this.root = createRoot(this.shadowRoot!);
this.cache = createCache({
key: "sample-element",
container: this.shadowRoot!,
});
this.setResetCss();
}
setResetCss() {
const style = document.createElement("style");
style.innerHTML = resetCss;
this.shadowRoot!.appendChild(style);
}
connectedCallback() {
this.root.render(
<CacheProvider value={this.cache}>
<SampleElement />
</CacheProvider>
);
}
}
customElements.define("sample-element", SampleElementComponent);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./src/index.tsx" type="module"></script>
<div>
<sample-element>
<span slot="test">slot show</span>
</sample-element>
</div>
</body>
</html>
解説
const slot = useRef<HTMLSlotElement>(null);
const result = useSlot(slot);
useRef
を利用してスロットを直接WebComponentsから参照できるようにします
はじめはnullの状態でuseSlot
が呼び出されます
const useSlot = (ref: React.RefObject<HTMLSlotElement>) => {
const [childNodes, setChildNodes] = useState<ChildNode[]>();
useEffect(() => {
const { current } = ref;
if (!current) return;
setChildNodes(current.assignedNodes() as ChildNode[]);
}, []);
return childNodes;
};
useSlotが実行されます。childNodes
というステートでスロットに割り当てられたノードを管理しています
コンポーネントのマウント時に引数で渡ってきたref
から要素(current
)を取得します
しかしこの段階ではnullなので、ifでreturnされてuseSlotは終わります
その後以下が描画されます
return (
<>
<slot name="test" ref={slot} style={{ display: "none" }} />
<div>no children</div>
</>
);
ref={slot}と指定しているので、この要素がDOMにマウントされたタイミングで、slot.currentはこの要素を指すようになります。
すると、useSlot
が再度実行されます
const useSlot = (ref: React.RefObject<HTMLSlotElement>) => {
const [childNodes, setChildNodes] = useState<ChildNode[]>();
useEffect(() => {
const { current } = ref;
if (!current) return;
setChildNodes(current.assignedNodes() as ChildNode[]);
}, []);
return childNodes;
};
今回はcurrent
にノードがあるので、setChildNodesにノードがセットされて返却されます
するとresultが存在するため以下が描画されます
if (result && result.length > 0) {
const [children] = result;
return (
<>
{[...Array(5)].map((_, n) => (
<div key={n}>
<span ref={(ref) => ref?.appendChild(children.cloneNode(true))}>
{n}
</span>
</div>
))}
</>
);
}
ここで、<span ref={(ref) => ref?.appendChild(children.cloneNode(true))}>
とすることで先程要素に対してrefコールバックを設定しています。
このコールバックは、要素がDOMにマウントされるタイミングで呼ばれ、その要素への参照(この場合はref)が引数として渡されます。
appendChild
でノードをクローンして追加しています
これを行うことで先程のuseSlotの返却した<slot name="test" ref={slot} style={{ display: "none" }} />
の要素が使えるようになります
slotを埋め込んだ状態のものをそのままコピーすることができるので何度も同じslotが利用できます
おわりに
教わったときにすごい仕組みだなと思ったので処理を追ってまとめてみました
参考