Custom Elementsでslotを1回だけ書いて何度も利用する

# はじめに

以前slotを何度も利用したいという記事を書きました

https://qiita.com/Sicut_study/items/ab10d9e91b268afeb793

ここではslotの名前を変えて対処を行いましたが、1つのslotを使い回す方法がわかったのでまとめます

# やり方

まずはコードをすべて載せます

```tsx:index.tsx
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);
```

```html:index.html
<!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>
```

# 解説

```tsx
  const slot = useRef<HTMLSlotElement>(null);
  const result = useSlot(slot);
```

`useRef`を利用してスロットを直接WebComponentsから参照できるようにします
はじめはnullの状態で`useSlot`が呼び出されます

```tsx
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は終わります

その後以下が描画されます

```tsx
  return (
    <>
      <slot name="test" ref={slot} style={{ display: "none" }} />
      <div>no children</div>
    </>
  );
```

ref={slot}と指定しているので、この<slot>要素がDOMにマウントされたタイミングで、slot.currentはこの<slot>要素を指すようになります。

すると、`useSlot`が再度実行されます

```tsx
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が存在するため以下が描画されます

```tsx
  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))}>`とすることで先程<span>要素に対してrefコールバックを設定しています。
このコールバックは、要素がDOMにマウントされるタイミングで呼ばれ、その要素への参照（この場合はref）が引数として渡されます。

`appendChild`でノードをクローンして追加しています
これを行うことで先程のuseSlotの返却した`<slot name="test" ref={slot} style={{ display: "none" }} />`の要素が使えるようになります

slotを埋め込んだ状態のものをそのままコピーすることができるので何度も同じslotが利用できます

# おわりに

教わったときにすごい仕組みだなと思ったので処理を追ってまとめてみました

# 参考

https://github.com/atomicojs/hooks/blob/master/src/use-slot/use-slot.js