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.tsactions はそのまま使えます。


② 送信中の状態を管理する

<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.tsfail() を使ってエラーを返します。

// 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() で返した textform.text に入る

use:enhance を使う・使わないの切り分け

判断基準は**「送信後も同じページに留まるか?」**です。留まるなら use:enhance、別ページに遷移するなら不要です。

場面 use:enhance
入力・追加・編集フォーム ✅ 使う
送信中の状態を見せたい ✅ 使う
バリデーションエラーをその場に表示 ✅ 使う
ログイン・ログアウト ❌ 不要(どうせ遷移する)
削除など1回きりの操作 ❌ 不要(どうせ画面が変わる)
JS無効環境を考慮 ❌ 使わない

まとめ

内容
use:enhance フォーム送信を fetch に置き換えてリロードなしにする
コールバックなし 自動でページデータを更新する
コールバックあり 送信中の状態管理や完了後の処理を追加できる
fail(400, {...}) バリデーションエラーをJSONでページに返す
form prop actions から返された値を受け取る
← トップページに戻る