SvelteKitの名前付きアクション(Named Actions)でフォームを整理した話
SvelteKitでは、1つのページに複数のフォームアクションを定義できます。これを Named Actions(名前付きアクション) と呼びます。TODOアプリの「追加・完了・削除・ログアウト」を例に整理します。
Named Actionsとは
通常のフォームアクションは actions に default を定義しますが、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を別途用意しなくてもサーバーアクションで完結するのが便利です。