SvelteKitの名前付きアクション(Named Actions)でフォームを整理した話

SvelteKitでは、1つのページに複数のフォームアクションを定義できます。これを Named Actions(名前付きアクション) と呼びます。TODOアプリの「追加・完了・削除・ログアウト」を例に整理します。


Named Actionsとは

通常のフォームアクションは actionsdefault を定義しますが、Named Actionsでは名前をつけて複数定義できます。

// +page.server.ts
export const actions: Actions = {
  create: async ({ request }) => { ... },
  delete: async ({ request }) => { ... },
};

フォーム側では action="?/アクション名" で呼び分けます。

<form method="POST" action="?/create">...</form>
<form method="POST" action="?/delete">...</form>

実装例:TODOアプリ

Todo型の定義

// src/lib/todo.ts
export type Todo = {
  id: number;
  title: string;
  done: boolean;
};

サーバー側(+page.server.ts)

import type { PageServerLoad, Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import type { Todo } from '$lib/todo';

let todos: Todo[] = [
  { id: 1, title: '牛乳を買う', done: false },
  { id: 2, title: '本を読む', done: false },
];
let nextId = 3;

export const load: PageServerLoad = ({ locals }) => {
  if (!locals.user) {
    redirect(303, '/login');
  }
  return { todos, user: locals.user };
};

export const actions: Actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const title = (data.get('title') as string).trim();

    if (!title) {
      return fail(400, { error: 'タイトルを入力してください' });
    }

    todos.push({ id: nextId++, title, done: false });
    redirect(303, '/todo');
  },
  complete: async ({ request }) => {
    const data = await request.formData();
    const id = Number(data.get('id'));

    todos = todos.map(t => t.id === id ? { ...t, done: !t.done } : t);
  },
  delete: async ({ request }) => {
    const data = await request.formData();
    const id = Number(data.get('id'));

    todos = todos.filter(t => t.id !== id);
  },
  logout: async ({ cookies }) => {
    cookies.delete('session_id', { path: '/' });
    redirect(303, '/login');
  },
};

クライアント側(+page.svelte)

<script lang="ts">
  import { enhance } from "$app/forms";
  let { data, form } = $props();
</script>

<h1>TODOリスト</h1>
<p>ようこそ、{data.user.name}さん</p>

<form method="POST" action="?/logout" use:enhance>
  <button type="submit">ログアウト</button>
</form>

<form method="POST" action="?/create" use:enhance>
  <input type="text" name="title" placeholder="タスクを入力" />
  <button type="submit">追加</button>
  {#if form?.error}
    <p style="color: red">{form.error}</p>
  {/if}
</form>

<ul>
  {#each data.todos as todo (todo.id)}
    <li style="text-decoration: {todo.done ? 'line-through' : 'none'}">
      {todo.title}

      <form method="POST" action="?/complete" use:enhance style="display: inline">
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit">{todo.done ? "戻す" : "完了"}</button>
      </form>

      <form method="POST" action="?/delete" use:enhance style="display: inline">
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit">削除</button>
      </form>
    </li>
  {/each}
</ul>

ポイントまとめ

hidden inputでIDを渡す

リスト項目ごとにフォームを作り、<input type="hidden" name="id" value={todo.id} /> でIDをサーバーに渡します。URLパラメータを使わずにシンプルに実現できます。

use:enhance との組み合わせ

use:enhance をつけることで、フルリロードなしに画面が更新されます。complete アクションのように redirect を返さない場合でも、SvelteKitが自動的に load を再実行してリストを更新します。

fail() でバリデーションエラーを返す

if (!title) {
  return fail(400, { error: 'タイトルを入力してください' });
}

fail() で返した値は、クライアント側で form プロップとして受け取れます。

{#if form?.error}
  <p style="color: red">{form.error}</p>
{/if}

default アクションとの違い

default アクション Named Actions
定義方法 actions = { default: ... } actions = { create: ..., delete: ... }
フォームのaction属性 省略可 ?/アクション名 が必要
複数アクション 不可

まとめ

Named Actionsを使うと、1ページ内の複数の操作をフォームだけでシンプルに実現できます。特にCRUD操作が必要なページでは、APIを別途用意しなくてもサーバーアクションで完結するのが便利です。

← トップページに戻る