結論
旧来のSvelteでは作りづらかったカスタムフック、Svelte5なら簡単に作れるようになったよ。例えばこんな感じ
useAsync.svelte.ts
export function useAsync<Fetcher extends (...args: any[]) => Promise<any>>(fetcher: Fetcher) {
let data = $state<Awaited<ReturnType<Fetcher>> | null>(null);
let error = $state<Error | null>(null);
let isPending = $state(false);
async function fetch(...args: Parameters<Fetcher>) {
isPending = true;
try {
const result = await fetcher(...args);
data = result;
error = null;
} catch (err) {
error = err as Error | null;
data = null;
} finally {
isPending = false;
}
}
return {
get data() {
return data;
},
get error() {
return error;
},
get isPending() {
return isPending;
},
fetch,
};
}
xxxx.svelte.ts ファイル内ではSvelte5で登場したRunesを使えるんだ。この関数のレスポンスそのものがリアクティブな値として返されるんだ。 応答を分割代入するとリアクティブ性が消えるから気をつけてね。(分割代入してもリアクティブ性を失いたくないなら、$derivedを介してやる必要がある)
usage
async function hoge() {
return "something";
}
const asyncExecutor = useAsync(hoge);
asyncExecutor.exec()
// asyncExecutor.data, asyncExecutor.isPending, asyncExecutor.errorがリアクティブになる
// ただし分割代入して使いたいなら、そのままだとリアクティブ性を失うので、やりたければ下記のようにする必要がある
// let {data, isPending, error} = $derived(asyncExecutor);
補足
とはいえ、そもそもSvelteには最初からPromiseをテンプレートに描画するためのシンタックスがあるからuseAsyncやuseFetchを用意せずとも、概ね同じことを実現できるんだよね。使い勝手は好みによるところがあると思うから、どちらを使うかはそこ次第かな。
{#await promise} <!-- promise is pending --> <p>waiting for the promise to resolve...</p> {:then value} <!-- promise was fulfilled or not a Promise --> <p>The value is {value}</p> {:catch error} <!-- promise was rejected --> <p>Something went wrong: {error.message}</p> {/await}
おまけ
Svelte5のカスタムフック集としてすごく参考になるページがあるから下記に掲載するね。