FetchOnMountコンポーネントとsvelte:boundary

## 概要
マウントされた時に渡した`Promise`を返す関数を実行し、以下を表示するコンポーネントを作ります。

- ローディング状態を表示する
- `Promise`の解決後の値を使って何かを表示する
    - その表示が失敗したら`svelte:boundary`を使って復帰する
- フォールバックを表示する

## `await`ブロックと`svelte:boundary`

Svelteには`{#await}`という構文があります。

```svelte
{#await promise}
{:then result}
{:catch error}
{/await}
```

ざっくりこのような構文です。

`{#await}`にはたとえばAPIへリクエストを投げる関数などを渡すことができます。

エラーが出た場合はcatchに入るのでアプリケーションが落ちないようにしたい場合はこれでいいのですが、
エラーから復帰したい場合は実装が必要であり、少し面倒です。

また、`{:then}`などの中でレンダリングしたコンポーネントの中で`Error`が投げられたりした場合は死んでしまうのでそのケアがしたい場合は`svelte:boundary`を使用すると良さそう（必要になったことはないですが…）です。

loading中に表示するコンポーネントとエラー時表示するコンポーネントは簡易なものが多いため`svelte:boundary`は必要ないことが多いと思いますが、`{:then}`で表示するコンポーネントは`svelte:boundary`が必要かもしれません。

ですが、毎回
```svelte
{#await promise}
{:then result}
  <svelte:boundary>
  　ここにエラーを返す可能性のあるコンポーネントを置く
    {#snippet failed(error, reset)}
    {/snippet}
  </svelte:boundary>
{:catch error}
{/await}
```

こんなの書いてられません。
さすがに面倒なのでコンポーネント化します。

## FetchOnMountコンポーネント
Svelteを書いている人にもあまり馴染みのないコードが出てくるかもしれません。
具体的にいうと`generics`です。

```svelte:FetchOnMount.svelte
<script
	lang="ts"
	generics="Params, Function extends (args: Params) => Promise<any>"
>
	import type { Snippet } from 'svelte';

	let {
		fetch,
		loadingRenderer,
		resultRenderer,
		fallbackRenderer,
	}: {
		fetch: {
			function: Function;
			params: Params;
		};
		loadingRenderer: Snippet;
		fallbackRenderer: Snippet<[{ errorMessage: string }]>;
		resultRenderer: Snippet<
			[
				{
					result: Function extends (args: Params) => Promise<infer Result>
						? Result
						: never;
				},
			]
		>;
	} = $props();
	let { function: fetchFunction, params: fetchFunctionParams } =
		$derived(fetch);
</script>

{#await fetchFunction(fetchFunctionParams)}
	{@render loadingRenderer()}
{:then result}
	<svelte:boundary>
		{@render resultRenderer({ result })}
		{#snippet failed(error, reset)}
			<p>resultRendererがエラーになりました。</p>
			<p>エラーメッセージ：{error}</p>
			<button type="button" onclick={reset}>リセットする</button>
		{/snippet}
	</svelte:boundary>
{:catch error}
	{#if error instanceof Error}
		{@render fallbackRenderer({ errorMessage: error.message })}
	{/if}
{/await}
```

Svelteでは`props`に`generics`を使用する場合、`script`に`generics`という属性を付与します。
正直嫌いよりの微妙だなと思うところではありますが、まぁ大体のことはできます。

今回は

- `Promise`を返す関数（とその引数）
- loading中に表示するスニペット
- `Promise`解決後に表示するスニペット
- エラー時のフォールバックで表示するスニペット

を引数として受け取りたいので、`props`の型は以下です。
`Function`と`Params`は`generics`で定義しています。

```ts
	let {
		fetch,
		loadingRenderer,
		resultRenderer,
		fallbackRenderer,
	}: {
		fetch: {
			function: Function;
			params: Params;
		};
		loadingRenderer: Snippet;
		fallbackRenderer: Snippet<[{ errorMessage: string }]>;
		resultRenderer: Snippet<
			[
				{
					result: Function extends (args: Params) => Promise<infer Result>
						? Result
						: never;
				},
			]
		>;
	} = $props();
 ```

また、`resultRenderer`ではPromise解決後の値を型つきで受け取って親に渡したいので、型は以下のようになっています。

```ts
Snippet<
  [
    {
      result: Function extends (args: Params) => Promise<infer Result>
        ? Result
        : never;
    },
  ]
>
```

:::note
余談

`generics="Params, Function extends (args: Params) => Promise<any>"`
の書き方に辿り着くまで長かったです…
argsの部分に`Params`をかけると思わなかったです。

ちなみに
`generics="Params, Result, Function extends (args: Params) => Promise<Result>"`
と書けはしますが、親側で
`{#snippet resultRenderer({ result })}`
の`result`に型がつきません。unknownになってしまいます。
:::

## Error.svelte

`{#snippet resultRenderer({ result })}`部分でエラーになるコンポーネントを適当に以下で作成しました。

```svelte:Error.svelte
<script lang="ts">
	let user = $state<{ message: string }>({ message: '' });
	const setMessageNull = () => {
		user = null as unknown as { message: string };
	};
</script>

<p>{user.message}</p>

<button type="button" onclick={setMessageNull}>
	絶対にクリックしないで！
</button>
```

「絶対にクリックしないで！」をクリックすると死にます。

`FetchOnMount`コンポーネントでは以下のように`svelte:boundary`でラップしているので死んだら`{#snippet failed(error, reset)}`の部分が表示され、リセットするボタンをクリックすると`{@render resultRenderer({ result })}`部分が再度表示されます。

```svelte
	<svelte:boundary>
		{@render resultRenderer({ result })}
		{#snippet failed(error, reset)}
			<p>resultRendererがエラーになりました。</p>
			<p>エラーメッセージ：{error}</p>
			<button type="button" onclick={reset}>リセットする</button>
		{/snippet}
	</svelte:boundary>
 ```

 ## 使う側
 ここまででFetchOnMountコンポーネントができたので表示してみましょう。

 ```svelte:+page.svelte
 <script lang="ts">
	import FetchOnMount from './FetchOnMount.svelte';
	import Error from './Error.svelte';

	const fetchFunction = async ({ message }: { message: string }) => {
		await new Promise((resolve) => setTimeout(resolve, 1000));
		return message;
	};
</script>

<FetchOnMount
	fetch={{
		function: fetchFunction,
		params: {
			message: 'Hello, world!',
		},
	}}
>
	{#snippet loadingRenderer()}
		<p>loading…</p>
	{/snippet}

	{#snippet resultRenderer({ result })}
		<p>{result}</p>
		<Error />
	{/snippet}

	{#snippet fallbackRenderer({ errorMessage })}
		<p style="color: red;">{errorMessage}</p>
	{/snippet}
</FetchOnMount>
```

先ほど`fetch`props部分の型に`generics`を使用したので、`fetch.params`の型が`fetch.function`の引数の型に縛れました。

また、`{#snippet resultRenderer({ result })}`の`result`部分は`fetch.function`の返り値の型がつきます。

## まとめ
ということで、Svelte5で実装された`generics`（正確にいうと$$GenericなどですでにSvelte4で実装はされていましたが…）と最近実装された`svelte:boundary`を使って便利なコンポーネントを作ってみました。

こちらのソースや実際の画面は以下にあるのでよかったらみてみてください。

- [ソース](https://github.com/shamokit/svelte5-examples/tree/main/src/routes/fetchOnMount)
- [デモページ](https://svelte5-examples.pages.dev/fetchOnMount)