Svelte5の新機能$hostルーンについて

Svelte5で新規に登場したルーンのうち、世間でまだあまり使用されていないルーンについての検証記事第１弾です。

`$host`ルーンはルーン紹介の末尾に記載されているのですが、公式チュートリアルだけではもう一つ何が便利なのかピンと来ませんでした。ですが、GitHub公式にあった以下の**仮説記事によると非推奨となったcreateDispatcherと同じことができるらしいので、$hostルーンで実験**してみました。

https://github.com/sveltejs/svelte/issues/11022

ただし、この`$host`ルーンは**カスタムエレメント**の使用を前提としています。そのため、ある程度カスタムエレメント作成の知識が必要です。

https://qiita.com/tronicboy/items/dd9bf434003d2f81c8b2

※カスタムエレメントとは、Reactでいうカスタムコンポーネントみたいなものですが、後述するように追加設定が必要です。

# 記事作成のきっかけ
大本になったのは自分がかつて投稿したcreateDispatchについての記事でしたが、もはや公式非推奨の技術をいつまでも残すわけには行かないと思ったからです。そして、テストに用いたのは[自分の記事](https://qiita.com/BRSF/items/605b605daabc36003876)で動作検証用に使用している演習６の買い物カゴシステムです。元の記事では`$props`を用いていますが、本記事ではそれを$hostルーン学習のためと、GitHubのissueにあった、制作者自らが残したこの仮説を元に、本当に動作再現できるか実証してみました。


> It would be useful to have a $host() rune, which would be a compile error without the relevant option.
> With this, we could treat createEventDispatcher as deprecated — IIRC the only real reason it > isn't already is that we need it to dispatch events from custom elements.

簡単にいうと「createEventDispatcherはカスタムイベントで処理するのに不備があったので、もう非推奨にしたよ。だから代わりに、不十分な使用法で正しくエラー制御される$hostルーンを使えばいいよ」ってことです。

# カスタムエレメントを作る
まずはディスパッチ用のカスタムエレメントを作成しましょう。それにはpluginに以下の設定が必要です。

```ts:Vite.config.ts
export default defineConfig({
  plugins: [svelte({
	compilerOptions: {
        customElement: true, //カスタムエレメントを自作できるようにする
      }
  })],
```

そして作成したカスタムエレメントがこれです。

## イベント用のカスタムエレメント

```svelte:EventDispatcher.svelte
<svelte:options customElement="event-dispatcher" />

<script>
const { name,caption } = $props();
const dispatch =(type)=>{
	$host().dispatchEvent(new CustomEvent(type));
}
</script>

<button onclick={()=> dispatch(name)}>{ caption }</button>
```

大事なポイントは`$props`で、このルーンからボタン作成に必要なキャプションとdispatchのモードを取得しています。

## カスタムエレメントを呼び出す
カスタムエレメントは以下のようにして呼び出します。

```svelte:Product.svelte
<script>
import { Link } from "svelte-routing"
import './EventDispatcher.svelte'; //カスタムエレメントをインポートする
const {reducer,context } = $props()
</script>
<main class="products">
  <ul>
    {#each $context.products as product,i}
    <li>
      <Link to="detail/{product.id}" class="li_product">
      <div >
        <strong>{ product.title }</strong> - { product.price }円
        {#if product.stock > 0} 【残り{ product.stock }個】 {/if}
      </div>
      </Link>
      <div>
      <event-dispatcher name="add" caption="買い物カゴにいれる" onadd={(e)=>reducer({mode:e.type,dif:product.id})} ></event-dispatcher>
      </div>
    </li>
    {/each}
  </ul>
</main>
```

カスタムエレメントはこの部分です。大事なのは`$host`ルーンでdispatchイベントを生成する際、**カスタムイベント名とdispatch関数の引数は同じ名前にする**必要があります。なのでイベント名は動的に変更できるように、nameで別途準備しておきます。

```svelte
//name = add はカスタムエレメントの$hostルーンによって分配された任意のイベント
//onadd はカスタムエレメント上のカスタムイベントを実行するためのハンドラ
//このnameプロパティの中身とハンドラのonを除いた2つの単語は同一にしておく必要がある。

<event-dispatcher name="add" caption="買い物カゴにいれる" onadd={(e)=>reducer({mode:e.type,dif:product.id})} ></event-dispatcher>
```

つまり、カスタムエレメント上で動的な変数を出力することで、`$host`ルーンのdispatcheEvent機能によって以下のような働きに変わります。また、captionにはボタンタグに表示したい文言を入力しておきます。

```svelte:EventDispatcher.svelte
<svelte:options customElement="event-dispatcher" />

<script>
const { name,caption } = $props(); //カスタムエレメント上にあるイベントからの引数
const dispatch =(type)=>{
	$host().dispatchEvent(new CustomEvent(type));
}
</script>

<button onclick={()=> dispatch(name)}>{ caption }</button>
<!-- <button onclick={()=> dispatch('add')}>買い物カゴにいれる</button> として機能している -->
```

## 他のコンポーネントでも利用する
この方法のメリットは他のコンポーネントでも機能を共用できることです。別の買い物かごページでも「買い物かごから戻す」ボタンと「購入」ボタンも同じカスタムエレメントを使用しています。

```svelte:cart.svelte
<script>
const {reducer,context } = $props()
import './EventDispatcher.svelte';
</script>
<main class="cart" >
 {#if $context.cart.length <= 0}<p>No Item in the Cart!</p>{/if}
  <ul>
  {#each $context.cart as cartItem,i}
      <li>
        <div>
          <strong>{ cartItem.title }</strong> - { cartItem.price }円
          ({ cartItem.quantity })
        </div>
        <div>
          <event-dispatcher name="remove" caption="買い物かごから戻す（1個ずつ）"
		  onremove={(e)=>reducer({mode:e.type,dif:cartItem.id})}></event-dispatcher>
        </div>
      </li>
  {/each}
  </ul>
  <h3>合計: {$context.total}円</h3>
  <h3>残高: {$context.money}円</h3>
  {#if $context.money >0 &&  $context.total >= 0}
  <event-dispatcher name="buy" caption="購入" onbuy={(e)=>reducer({mode:e.type,dif:null})}></event-dispatcher>
  {/if}
</main>
```

## カスタムエレメントのイベント受取
カスタムイベントのメリットはイベントを取得できるので、以下のように**e.type**としてイベントから引数を用いれば、遷移先のメソッドで使用した機能を利用することができます。その場合onhogeでハンドルしたhogeが戻り値となります。

```
hoge((e)=>(e.type))
```

### イベントを受け取る
イベントの受取先では、以下のようになっています。`$props()`ルーンのイベントバブリングからは、カスタムイベントの種類まで受け取れません。なので、先程の段階で準備しておく必要があります。

```svelte:GlobalState.svelte
<script>
/*中略*/
function reducer(ev){
	const mode = ev.mode //機能分岐（カスタムイベント上の引数e.typeを取得する）
	const dif = ev.dif //更新差分
	let store = [];
	context.subscribe((x)=> store = x) //ストアオブジェクトを監視対象とする
	shopReducer(mode,dif,store)
	context.update(() => store) //分割代入で更新する
}

<Router>
  <ul class="main-navigation">
    <nav>
        <li><Link to="/products" class="nav">Products({cnt_articles})</Link></li>
        <li><Link to="/cart" class="nav">Cart({cnt_cart})</Link></li>
    </nav>
  </ul>
  <main>
    <Route path="detail/:id" let:params ><Detail {context} selid={params.id} /></Route>
    <Route path=""><Products {context} {reducer} /></Route>
    <Route path="cart"><Cart {context} {reducer} /></Route>
  </main>
</Router>
```

## 利用の感想
正直、公式サイトでも紹介が微妙な上に、海外含め誰も記事で紹介していないことから、`$shost`ルーンの機能は発展途上だと思いますし、改良余地もあると思います。現状、`$props`ルーンで十分代用できるので、敢えてシステムが煩雑化してイベントとデータの分別が必要になったとき、この方法が役立ってくるのではないでしょうか。

## 公式の`$host`ルーン紹介

https://svelte.jp/docs/svelte/$host