1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

概要

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

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

awaitブロックとsvelte:boundary

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

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

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

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

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

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

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

ですが、毎回

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

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

FetchOnMountコンポーネント

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

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ではpropsgenericsを使用する場合、scriptgenericsという属性を付与します。
正直嫌いよりの微妙だなと思うところではありますが、まぁ大体のことはできます。

今回は

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

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

	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解決後の値を型つきで受け取って親に渡したいので、型は以下のようになっています。

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

余談

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 })}部分でエラーになるコンポーネントを適当に以下で作成しました。

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

使う側

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

+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>

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

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

まとめ

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?