祝! Remote functions、Async SSR
みなさん、先日強化されたRemote functionsは使っていますか?
Remote functionsは以前まで<svelte:boundary>でラップしないとエラーになり、
SSRされるのはloading部分だけで、JavaScriptが無効になっている場合そこから先が表示されないという状況でした。
先日のリリースでついに
<script lang="ts">
import { getPosts } from './data.remote';
</script>
<ul>
{#each await getPosts() as { title, slug }}
<li><a href="/blog/{slug}">{title}</a></li>
{/each}
</ul>
このように書くことができるようになり、今までと大きくコンポーネントやfetchデータ周りの設計が変わることを予感しています。
さてそんなRemote functions(以下RF)ですが、最近でたばかりの機能なのでまだ慣れていない人も多いのではないでしょうか?
かくいう私もそうで、RFに生えているrefreshが動かず、なかなかミスに気づけず困ったので供養がてら事例を紹介します。
ソース
以下に長々書いていますが、自分で読んでもわかりにくかったのでコードをみてください。
(ごめんなさい!)
https://github.com/shamokit/remote-functions/tree/main/src
動かなかった事例
GetPosts、posts、posts2、NoWorkingGetPostsはそれぞれRFのqueryを使ってランダムなidを返すpostsを表示しています。
内部で呼んでいるRFの中身は
import * as v from 'valibot';
import { query } from '$app/server';
export const getPosts = query(v.string(), async (p: string) => {
const id = crypto.randomUUID();
const posts = [
{
id: id,
title: 'Post 1',
content: 'Content 1',
},
{
id: p,
title: 'Post 2',
content: 'Content 2',
},
];
return posts;
});
こうなっています。
RFは引数を取ることができ、同じ引数を渡したものは同期されます。
つまり、RF('a')とRF('b')は別々に同期され、RF('a').refresh()を呼んだ場合はawait RF('a')の部分が全て同期されます。
逆にRF('b')は更新されません。
ここで、
GetPostsposts in templateposts in template with derivedpostsNoWorkingGetPosts
に関してはRF('a')を呼んでおり
posts2
はRF('b')を呼んでいます。
ボタンを押して一緒に更新が走るか試してみましょう!
( 何個か更新されません。予想しよう!)
先ほどあげたWebページでrefresh postsボタンを押すと
GetPostsposts in template with derivedposts
が同時に更新され、
refresh posts2ボタンを押すと
posts2
だけが更新されるのがわかります。
posts in template、NoWorkingGetPostsは更新されませんが、これが引っかかった部分です。
同じRF('a')を呼んでいるのになぜでしょうか?
答え:scriptブロックの中身は最初のrender時実行される
posts in template、NoWorkingGetPostsはソースをみるとscriptブロックの中でawaitしてしまっています。
これがミスでした。
scriptブロックで書いたものは最初一回しか実行されないのが原因です。
原因というか普通にミスです。
ついでにいうと、posts in template with derivedに関してはRF('a')の部分を$derived(await getPosts('a'))のように$derivedで囲っているので再実行されます。
おもしろいですね。
まとめ
Remote functionsはいいぞ!
けどなんとなく使うと引っかかるかも!?という記事でした。
個人的に思っているよさげな方針
個人的には
<script lang="ts">
import { getPosts } from './data.remote';
</script>
<ul>
{#each await getPosts() as { title, slug }}
<li><a href="/blog/{slug}">{title}</a></li>
{/each}
</ul>
こういうコンポーネントを作ってしまうと
- テストが書きづらくなったりするかも
- コンポーネントが
RFに直接依存するのは避けたい
と思ったので、
<GetPosts>
{#snippet children(posts)}
<Posts posts={await posts} />
{/snippet}
</GetPosts>
ざっくりこんな感じでRFを呼ぶ箇所とそれを使う箇所をまとめて影響範囲がわかるようにした上で、
表示に関する部分はRFに依存しない形でテストしやすいように作っておくかなぁと思いました。
まぁ好みかなとは思います。
みなさんはどう書いていますか?
また、refetch query.batch周りは便利な反面影響範囲がどこまで広がるかを気にしておく必要があって後々辛くなる予感がしたので気をつけたほうがいいかもしれないと思っています。
ただ今までのように+page.server.tsに依存する設計だとそこがどうしても膨らんだりフォームがそこに依存したりで微妙なところもあったのが解消されたので、かなり嬉しいアップデートだなと思っています。
あとNetworkタブをみるとgetPostsがボタンの押下一回につき一回しか呼ばれないのですごい…
まとめ
おわり!
みなさんもいろんな使い方を共有してRFとSvelte界隈を盛り上げ、React界隈を怖がらせましょう!