Svelte でコンポーネントを作り React で糊付けする(ペライチ React・非実用的編)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187200/2dc0ed99-ce09-4982-ad02-355225b15bf9.png" width="100%">

# はじめに

[前回](https://qiita.com/uniho/items/bf00c2eb5b24e4212a2f) は、よくわからないことを書いて自己満足していましたが、今回はその続きとして非実用的なコードを示し、引き続き自己満足に浸りたいと思います（わかる人だけわかってくればいいのです!😭）。

# サンプルコード

私が得意なペライチ React なソースコードです。誰が何といおうと私はペライチ React を布教していく所存です。
というか、ペライチ React を理解できていないと、「上級SE氏～、なぜかバンドラがうまく動かないんですぅ～」レベルから脱却できず給料も上がらないと思います。

``` index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="initial-scale=1, width=device-width" />
  </head>
  <body>
    <div id="app"></div>

    <script type="importmap">
      {
        "imports": {
          "svelte/": "https://esm.sh/svelte@5/"
        }
      }
    </script>

    <script type="module">

import React from "https://esm.sh/react@19?dev";
import ReactDOM from "https://esm.sh/react-dom@19?dev";
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev";

import {VERSION, compile} from 'svelte/compiler';
import {mount, unmount} from 'svelte/';

//
const sv = compile(`
  <script>
    const {name = 'world'} = $props();
  <\/script>   <!-- ペライチの場合、\ を付けないと script タグの閉じと誤認識されてしまう? -->

  <h1>Hello {name}!</h1>

  <style>
    h1 {
      color: red;
    }
  </style>
`, {/*css: 'injected'*/});

console.log(sv.js.code, sv.css.code);

//
const App = props => {

  const refDiv = React.useRef();

  React.useEffect(() => {
    const controller = new AbortController();

    (async() => {
      const SVC = await import(`data:text/javascript,${sv.js.code}`);
      const cssModule = await import(`data:text/css,${sv.css.code}`, {with: {type: 'css'}});
  
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, cssModule.default];
      const svc = mount(SVC.default, {target: refDiv.current, props: {name: `Svelte ${VERSION}`}});

      controller.signal.addEventListener("abort", () => {
        document.adoptedStyleSheets = document.adoptedStyleSheets.filter(sheet => sheet !== cssModule.default);
        unmount(svc);
      });
    })();  

    return () => {
      controller.abort();
    };
  }, []);

  return React.createElement('div', {ref: refDiv});
}

const root = ReactDOMClient.createRoot(document.getElementById("app"))
root.render(React.createElement(App))

    </script>

  </body>
</html>
```

実行すると次のような感じの描画になるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187200/bb5abd14-cfc7-4449-9bdf-97a5829592cc.png)

# 解説

``` html
    <script type="importmap">
      {
        "imports": {
          "svelte/": "https://esm.sh/svelte@5/"
        }
      }
    </script>
```
インポートマップの指定です。これにより JS にコンパイルした Svelte ソースを直接実行することができるようになります。
古いブラウザにも対応したい場合は [guybedford / es-module-shims](https://github.com/guybedford/es-module-shims) を polyfill として利用するのがよいと思います。

``` js
import React from "https://esm.sh/react@19?dev";
import ReactDOM from "https://esm.sh/react-dom@19?dev";
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev";
```

ペライチ React では `esm.sh` という CDN から React をロードして使います。

``` js
import {VERSION, compile} from 'svelte/compiler';
```
今回は Svelte のコンパイラを利用しますのでその定義をします。通常は、コンパイル済みのソースをサーバーにアップロードしたり、サーバーサイドでコンパイルしたりすると思いますのでその場合は不要です。

``` js
import {mount, unmount} from 'svelte/';
```
Svelte コンポーネントをマウントしたりアンマウントする API です。ImprotMap を定義済みなので `from 'svelte/'` と書けます。

``` js
const sv = compile(`・・・
```
Svelte コンポーネントを JS にコンパイルしています。

``` js
const App = props => {
```
`App` という React コンポーネントを定義します。

``` js
  const refDiv = React.useRef();
```
`App` コンポーネントの DOM エレメントを格納するための ref を準備します。

``` js
  React.useEffect(() => {
```
Side Effect を定義します。なお、Side Effect を日本語にすると「副作用」で、何か予想外の悪いことが起きそうなイメージが付きまといますがそうではありません。React などの DOM 操作ライブラリの類における Side Effect は「描画以外の処理」くらいの意味です。なので私は「副作用」とはいわず、あえて - Side Effect - というように意識して使っています。

``` js
    const controller = new AbortController();
```
非同期処理のクリーンアップ用の `AbortController` を準備します。React の Side Effect 部分で非同期処理を書く場合は必須といっても過言ではありません。これがないと中途半端な非同期処理が残ってどんどんあなたのかわいい WEB アプリが不安定になっていくはずです（知らんけど）。

``` js
      const SVC = await import(`data:text/javascript,${sv.js.code}`);
```
Svelte コンポーネントをインポートしています。
もしかしたら、ここでモジュールスコープの変数である `sv` を使用するのは、いわゆる React の Pure ルールに反すると考える方もおられるのかもしれませんのでその点についても解説しておきたいと思います。
まず、Pure ルールは、関数型プログラミングで求められる概念で、その概念を取り入れることでエンジニアリングの生産性を向上させようとするものです。React もその概念を取り入れることで、同じ目的を達成しようと考えているものと思います。
しかしながら、関数型プログラミングの人気は、お世辞にも高いとは言えません。その理由として考えられるのは、①言語自体が関数型プログラミング専用言語でないとどこかで妥協せざるを得なくなる、②低レベルなAPIなどまで関数型プログラミングでないとどこかで妥協せざるを得なくなること、などにあると私は考えております。
JavaScript は関数型プログラミング専用言語ではないし、APIなども関数型プログラミング専用ではありませんので、例えば「object型の内容を変更しないでね、あっ初期化所のときは別よ、初期化処理とは・・・」や、「ブラウザの API を呼ぶのは関数型じゃなくてよいよ」みたいな、本来の pure ルールを緩めた独自の「ただし書き」を追加せざるをえんわけです。限界があるなら最初から普通にコーディングしようぜ、みたいになるのは仕方ないと感じます。
さて、React に話を戻しますと、React での Pure 性は React が仮想 DOM 操作ライブラリ＋αに過ぎない以上仮想 DOM に関わるものが対象という「ただし書き」があると思いますので（そこを超えるものだとしたらさすがに越権すぎるとしてボイコットするユーザーが増えるはず）、Side Effect 内で完結するものである限り Pure ルールとは無関係ということになります(仮想 DOM に変更を加えるものはもはや `Side` Effect ではなく、`Main` Effect と言い換えられるかもしれません)。
なお、Pure ルールを守りたいが React が準備している標準の hook だけでは満足できないというユーザーが多いから、`Jotai` という素晴らしいライブラリが人気があったりするのだと思います（`Jotai` の素晴らしさを分かってもらうための記事もいつか書きたいと思っています）。

ひとつだけ誤解しないでいただきたいのは、「関数型プログラミングの貫徹度」を決めるのはあなたもしくはあなたのプロジェクトの総意だけで十分ということです。誰から押し付けられるものでもなく誰に押し付けるものでもないということです。ですから、「たとえ　Side Effect であっても関数型プログラミングを貫徹するのだ」というのは全然ありです。逆に「私が思う関数型プログラミングはこうだから君たちも実践しなさい。信じる者は救われる」というのもやめましょう。多くの場合嫌われるだけです。


``` js
      const cssModule = await import(`data:text/css,${sv.css.code}`, {with: {type: 'css'}});
```
Svelte コンポーネントが使う CSS を CSS Module としてインポートしています。
なお、古いブラウザについては既出の `es-module-shims` が polyfill として使えるはずです。
また、コンパイルオプションで `css: 'injected'` を指定すれば、CSS を Svelte 任せにできるはずです。

``` js
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, cssModule.default];
```
インポートした CSS を DOM に追加しています。

``` js
      const svc = mount(SVC.default, {target: refDiv.current, props: {name: `Svelte ${VERSION}`}});
```
インポートした Svelte コンポーネントをマウントし React と結合しています。

``` js
      controller.signal.addEventListener("abort", () => {
        document.adoptedStyleSheets = document.adoptedStyleSheets.filter(sheet => sheet !== cssModule.default);
        unmount(svc);
      });
```
React コンポーネントがアンマウントされたときのクリーンアップ処理です。Svelte 関係の処理が非同期で行われるためこういう感じで処理する必要があります。
他にもやり方はあるでしょうが、個人的に今のところこれがベストと考えています。
スタイルシートの除去処理が分かりにくいかもしれませんが、VS Code の Copilot AI が勝手にこのコードを提示してくれました。便利な時代になったものです。

``` js
    return () => {
      controller.abort();
    };
```
`useEffect` のクリーンアップ処理です。既出の `abort` イベントが呼ばれるはずです。

``` js
  return React.createElement('div', {ref: refDiv});
```
App コンポーネントの中身はこれだけで、あとは Svelte コンポーネントの範疇となります。なお、JSX ですと `return (<div ref={refDiv}></div>);` です。

``` js
const root = ReactDOMClient.createRoot(document.getElementById("app"))
root.render(React.createElement(App))
```
React を起動して App コンポーネントを描画します。

# 最後に
いかかがでしたでしょうか？大変面白かったでしょう？いいんです自己満足です。
わかる人だけわかってくればいいのです!😭
ではまたあいましょう。
Let’s have fun with React and Svelte!