はじめに
SvelteKit 2.27 で導入されたRemote Functionsは、クライアントとサーバー間の通信を革新的に簡単にする実験的機能です。この機能により、サーバーサイドの関数をあたかもローカル関数のように呼び出すことができ、型安全性を保ちながら開発体験を大幅に向上させます。
この記事で学べること
- Remote Functions の基本概念と仕組み
- 4 つの関数タイプ(query、form、command、prerender)の使い方
- 実践的な実装パターンとベストプラクティス
- 従来の方法との比較と移行方法
Remote Functionsとは?
Remote Functions は、.remote.ts
ファイルに定義した関数を、クライアントサイドから直接呼び出せるようにする機能です。これらの関数は常にサーバー上で実行され、環境変数やデータベースクライアントなどのサーバー専用リソースに安全にアクセスできます。
動作フロー
sequenceDiagram
participant Client as クライアント
participant Server as サーバー
participant DB as データベース
Client->>Server: Remote Function呼び出し
Server->>DB: データ取得/更新
DB-->>Server: 結果返却
Server-->>Client: 型安全な応答
Client->>Client: UIを更新
セットアップ
Remote Functions を使用するには、まず svelte.config.js
で機能を有効化する必要があります:
export default {
kit: {
experimental: {
remoteFunctions: true,
},
},
};
⚠️ 実験的機能についての注意
Remote Functions は現在実験的機能です。本番環境での使用は慎重に検討し、将来的な API の変更に備えてください。
4つの関数タイプ
Remote Functions には、用途に応じて 4 つのタイプが用意されています:
タイプ | 用途 | 実行タイミング | HTMLフォーム対応 |
---|---|---|---|
query | データ読み取り | コンポーネントレンダリング時 | なし |
form | フォーム送信 | フォーム送信時 | あり |
command | 任意のデータ書き込み | 任意のタイミング | なし |
prerender | 静的データ生成 | ビルド時 | なし |
1. Query関数 - データの読み取り
Query 関数は、サーバーから動的にデータを読み取るために使用します:
// src/routes/posts.remote.ts
import { query } from '@sveltejs/kit/remote';
import { db } from '$lib/database';
export const getPosts = query(async () => {
const posts = await db.sql`
SELECT id, title, slug, excerpt, published_at
FROM posts
WHERE published = true
ORDER BY published_at DESC
LIMIT 10
`;
return posts;
});
<!-- src/routes/+page.svelte -->
<script>
import { getPosts } from './posts.remote';
const posts = getPosts();
</script>
{#await posts}
<p>記事を読み込み中...</p>
{:then postList}
<ul>
{#each postList as post}
<li>
<a href="/blog/{post.slug}">{post.title}</a>
</li>
{/each}
</ul>
{:catch error}
<p>エラー: {error.message}</p>
{/await}
2. Form関数 - フォーム送信処理
Form 関数は、HTML フォームと連携して動作し、プログレッシブエンハンスメントをサポートします:
// src/routes/contact.remote.ts
import { form } from '@sveltejs/kit/remote';
import { z } from 'zod';
const contactSchema = z.object({
name: z.string().min(1, '名前は必須です'),
email: z.string().email('有効なメールアドレスを入力してください'),
message: z.string().min(10, 'メッセージは10文字以上入力してください'),
});
export const submitContact = form(contactSchema, async ({ name, email, message }) => {
// メール送信処理
await sendEmail({
to: 'admin@example.com',
subject: `お問い合わせ: ${name}`,
body: `${email}から:\n${message}`,
});
return { success: true };
});
<!-- src/routes/contact/+page.svelte -->
<script>
import { submitContact } from './contact.remote';
import { enhance } from '$app/forms';
</script>
<form method="POST" use:enhance={submitContact}>
<label>
名前:
<input name="name" required />
</label>
<label>
メール:
<input type="email" name="email" required />
</label>
<label>
メッセージ:
<textarea name="message" required></textarea>
</label>
<button type="submit">送信</button>
</form>
3. Command関数 - 任意のデータ操作
Command 関数は、任意のタイミングでサーバー上でデータ操作を実行できます:
// src/routes/todos.remote.ts
import { command } from '@sveltejs/kit/remote';
import { z } from 'zod';
export const toggleTodo = command(z.object({ id: z.number() }), async ({ id }) => {
const todo = await db.todo.update({
where: { id },
data: {
completed: { not: true },
},
});
return todo;
});
<script>
import { toggleTodo } from './todos.remote';
async function handleToggle(id) {
try {
await toggleTodo({ id });
// UIを更新
} catch (error) {
console.error('エラー:', error);
}
}
</script>
<button onclick={() => handleToggle(todo.id)}>
完了/未完了を切り替え
</button>
4. Prerender関数 - 静的データ生成
Prerender 関数は、ビルド時に一度だけ実行され、静的なデータを生成します:
// src/routes/config.remote.ts
import { prerender } from '@sveltejs/kit/remote';
export const getSiteConfig = prerender(async () => {
// ビルド時に一度だけ実行
const config = await loadConfigFromCMS();
return {
siteName: config.name,
description: config.description,
socialLinks: config.socialLinks,
};
});
高度な機能
バリデーション
Zod などのスキーマライブラリを使用して、入力値を自動的に検証できます:
import { query } from '@sveltejs/kit/remote';
import { z } from 'zod';
const searchSchema = z.object({
query: z.string().min(1).max(100),
category: z.enum(['tech', 'life', 'business']).optional(),
limit: z.number().min(1).max(50).default(10),
});
export const searchPosts = query(searchSchema, async ({ query, category, limit }) => {
// 型安全なパラメータで検索実行
const results = await db.posts.search({
query,
category,
limit,
});
return results;
});
オプティミスティック更新
Command 関数でオプティミスティック更新を実装する例:
<script>
import { updateProfile } from './profile.remote';
let profile = $state({ name: 'John', bio: 'Developer' });
let saving = $state(false);
async function saveProfile() {
saving = true;
const optimisticProfile = { ...profile };
try {
// 楽観的にUIを更新
profile = optimisticProfile;
// サーバーに送信
const result = await updateProfile(optimisticProfile);
// サーバーの結果で更新
profile = result;
} catch (error) {
// エラー時は元に戻す
profile = originalProfile;
console.error('保存失敗:', error);
} finally {
saving = false;
}
}
</script>
従来の方法との比較
従来の方法(+page.server.ts)
// +page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => {
const response = await fetch('/api/posts');
const posts = await response.json();
return {
posts,
};
};
// +page.svelte
export let data;
const { posts } = data;
Remote Functions を使用した方法
// posts.remote.ts
export const getPosts = query(async () => {
return await db.posts.findMany();
});
// +page.svelte
import { getPosts } from './posts.remote';
const posts = getPosts();
Remote Functions を使用することで、ボイラープレートコードが削減され、より直感的な実装が可能になります。
ベストプラクティス
1. エラーハンドリング
適切なエラーハンドリングを実装しましょう:
export const riskyOperation = command(z.object({ id: z.number() }), async ({ id }) => {
try {
const result = await performOperation(id);
return { success: true, data: result };
} catch (error) {
// エラーログを記録
console.error('Operation failed:', error);
// クライアントに適切なエラーを返す
throw new Error('操作に失敗しました。後でもう一度お試しください。');
}
});
2. セキュリティ
認証と認可を適切に実装:
import { query } from '@sveltejs/kit/remote';
import { getUserFromSession } from '$lib/auth';
export const getUserProfile = query(async (event) => {
const user = await getUserFromSession(event);
if (!user) {
throw new Error('認証が必要です');
}
return await db.users.findUnique({
where: { id: user.id },
select: {
id: true,
name: true,
email: true,
// パスワードなどの機密情報は除外
},
});
});
3. パフォーマンス最適化
必要に応じてキャッシュを実装:
const cache = new Map();
export const getCachedData = query(async () => {
const cacheKey = 'expensive-data';
if (cache.has(cacheKey)) {
const { data, timestamp } = cache.get(cacheKey);
// 5分以内のキャッシュは有効
if (Date.now() - timestamp < 5 * 60 * 1000) {
return data;
}
}
const data = await expensiveOperation();
cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
});
今後の展望
ロードマップ
- 2024年秋: Remote Functions 実験的リリース(SvelteKit 2.27)
- 2025年春: 現在 - コミュニティフィードバック収集中
- 2025年秋(予定): 次期メジャーバージョンで実験的フラグ解除予定
- 将来: キャッシング、バッチング、ストリーミング対応
検討中の機能
開発チームは以下の機能追加を検討しています:
- サーバーサイドキャッシング: クエリ結果の自動キャッシュ
- クライアントサイドキャッシング: TanStack Query 風のキャッシュ管理
- クエリバッチング: 複数のクエリを 1 つのリクエストにまとめる
- ストリーミングデータ: リアルタイムデータの対応
まとめ
SvelteKit Remote Functions は、フルスタック開発の新しいパラダイムを提示しています。型安全性を保ちながら、クライアント・サーバー間の境界を意識せずに開発できることで、開発速度と保守性が大幅に向上します。
Remote Functions の主な利点
- 型安全性: TypeScript の型推論が完全に機能
- 開発体験: ボイラープレートコードの削減
- 保守性: コードの局所性による理解しやすさ
- 柔軟性: 4 つの関数タイプで様々なユースケースに対応
実験的機能であることを考慮しつつ、新しいプロジェクトや小規模な機能から導入を検討してみてはいかがでしょうか。