概要
マウントされた時に渡した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
です。
<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
で定義しています。
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 })}
部分でエラーになるコンポーネントを適当に以下で作成しました。
<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コンポーネントができたので表示してみましょう。
<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
を使って便利なコンポーネントを作ってみました。
こちらのソースや実際の画面は以下にあるのでよかったらみてみてください。