SvelteKitのuse:enhanceによるフォームの段階的強化を整理した話
SvelteKitのフォームはデフォルトでフルページリロードして送信されます。use:enhance を使うとリロードなしで送信できるようになります。仕組みと使い分けを整理します。
デフォルトの動作(use:enhance なし)
<form method="POST">
<input name="text" />
<button>送信</button>
</form>
送信するたびにページ全体がリロードされて画面がチラつきます。
① use:enhance を追加する
<script lang="ts">
import { enhance } from '$app/forms';
</script>
<form method="POST" use:enhance>
<input name="text" />
<button>送信</button>
</form>
use:enhance を付けるだけでリロードなしになります。+page.server.ts の actions はそのまま使えます。
② 送信中の状態を管理する
<script lang="ts">
import { enhance } from '$app/forms';
let submitting = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
submitting = true;
return async ({ update }) => {
await update();
submitting = false;
};
}}
>
<input name="text" placeholder="タスクを入力" />
<button disabled={submitting}>
{submitting ? '送信中...' : '追加'}
</button>
</form>
コールバックを渡すことで「送信開始〜完了」の間の状態を制御できます。return async ({ update }) => { await update(); } の部分はサーバーのレスポンスを受け取ってページデータを更新する処理です。
③ バリデーションエラーを表示する
+page.server.ts で fail() を使ってエラーを返します。
// src/routes/learn/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const text = data.get('text') as string;
if (!text || text.trim() === '') {
return fail(400, {
error: 'テキストを入力してください',
text, // 入力値を返すと入力欄に残せる
});
}
// 正常処理
return { success: true };
},
};
<!-- src/routes/learn/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
type Props = {
form: ActionData;
};
let { form }: Props = $props();
let submitting = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
submitting = true;
return async ({ update }) => {
await update();
submitting = false;
};
}}
>
<input
name="text"
value={form?.text ?? ''}
placeholder="タスクを入力"
/>
{#if form?.error}
<p style="color: red">{form.error}</p>
{/if}
<button disabled={submitting}>
{submitting ? '送信中...' : '追加'}
</button>
</form>
リロードなしで画面が更新される仕組み
use:enhance はブラウザのデフォルトのフォーム送信を横取りして、裏で fetch に置き換えています。
① フォーム送信(use:enhance がフックする)
② ページリロードをキャンセル
③ fetch で同じリクエストをバックグラウンドで送信
④ サーバーから JSON でレスポンスを受け取る
⑤ $state が更新される → 画面が再描画される
通常のフォーム送信はHTMLページを返しますが、use:enhance 経由ではJSONを返します。
// 通常のフォーム送信
→ サーバーが HTML ページを返す → ブラウザが画面全体を描画
// use:enhance 経由
→ サーバーが JSON を返す → SvelteKit が差分だけ更新
fail(400, { error: 'テキストを入力してください' }) と書いたとき、サーバーは以下のようなJSONを返しています。
{
"type": "failure",
"status": 400,
"data": {
"error": "テキストを入力してください",
"text": ""
}
}
update() を呼ぶとSvelteKitが受け取ったJSONを form propに反映します。form は $props() で受け取った値なので、値が変わるとSvelteのリアクティビティが働いて画面が再描画されます。
一方、submitting の更新はサーバーと無関係なローカルの $state の切り替えです。
| 何が変わるか | どうやって変わるか |
|---|---|
| エラーメッセージ | fail() のJSON → update() → form prop → リアクティビティ |
| ボタンのテキスト | submitting という $state を切り替えるだけ |
| フォームの入力値 | fail() で返した text が form.text に入る |
use:enhance を使う・使わないの切り分け
判断基準は**「送信後も同じページに留まるか?」**です。留まるなら use:enhance、別ページに遷移するなら不要です。
| 場面 | use:enhance |
|---|---|
| 入力・追加・編集フォーム | ✅ 使う |
| 送信中の状態を見せたい | ✅ 使う |
| バリデーションエラーをその場に表示 | ✅ 使う |
| ログイン・ログアウト | ❌ 不要(どうせ遷移する) |
| 削除など1回きりの操作 | ❌ 不要(どうせ画面が変わる) |
| JS無効環境を考慮 | ❌ 使わない |
まとめ
| 内容 | |
|---|---|
use:enhance |
フォーム送信を fetch に置き換えてリロードなしにする |
| コールバックなし | 自動でページデータを更新する |
| コールバックあり | 送信中の状態管理や完了後の処理を追加できる |
fail(400, {...}) |
バリデーションエラーをJSONでページに返す |
form prop |
actions から返された値を受け取る |