3
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?

Remote functionsで詰まった〜

Last updated at Posted at 2025-09-29

祝! Remote functionsAsync 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

動かなかった事例

GetPostspostsposts2NoWorkingGetPostsはそれぞれRFqueryを使ってランダムな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')は更新されません。

ここで、

  • GetPosts
  • posts in template
  • posts in template with derived
  • posts
  • NoWorkingGetPosts

に関してはRF('a')を呼んでおり

  • posts2

RF('b')を呼んでいます。

ボタンを押して一緒に更新が走るか試してみましょう!
何個か更新されません。予想しよう!


先ほどあげたWebページでrefresh postsボタンを押すと

  • GetPosts
  • posts in template with derived
  • posts

が同時に更新され、

refresh posts2ボタンを押すと

  • posts2

だけが更新されるのがわかります。

posts in templateNoWorkingGetPostsは更新されませんが、これが引っかかった部分です。
同じRF('a')を呼んでいるのになぜでしょうか?

答え:scriptブロックの中身は最初のrender時実行される

posts in templateNoWorkingGetPostsはソースをみると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界隈を怖がらせましょう!

3
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
3
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?