Skip to main content

Core concepts

Form actions

Edit this page on GitHub

+page.server.js ファイルは actions をエクスポートできます。これによって、<form> 要素を使用することでサーバーにデータを POST することができます。

<form> を使用する場合、クライアントサイドの JavaScript はオプションですが、JavaScript によって form のインタラクションを簡単にプログレッシブに強化(progressively enhance)することができ、最高のユーザーエクスペリエンスを提供することができます。

Default actions

最もシンプルなケースでは、ページは default の action を宣言します:

src/routes/login/+page.server.js
ts
/** @type {import('./$types').Actions} */
export const actions = {
default: async (event) => {
// TODO log the user in
}
};
src/routes/login/+page.server.ts
ts
import type { Actions } from './$types';
export const actions = {
default: async (event) => {
// TODO log the user in
},
} satisfies Actions;

/login ページからこの action を呼び出すには、<form> を追加します。JavaScript は必要ありません:

src/routes/login/+page.svelte
<form method="POST">
	<label>
		Email
		<input name="email" type="email">
	</label>
	<label>
		Password
		<input name="password" type="password">
	</label>
	<button>Log in</button>
</form>

もし誰かがボタンをクリックしたら、ブラウザは form のデータを POST リクエストでサーバーに送信し、デフォルトの action が実行されます。

Action は常に POST リクエストを使用します。GET リクエストには決して副作用があってはならないからです。

また、action 属性を追加し、リクエスト先のページを指し示すことで、他のページから action を呼び出すこともできます (例えば、最上位のレイアウト(root layout)にある nav にログイン用の widget がある場合):

src/routes/+layout.svelte
<form method="POST" action="/login">
	<!-- content -->
</form>

Named actions

単一の default の action の代わりに、名前付きの action (named action) を必要なだけ持つことができます:

src/routes/login/+page.server.js
/** @type {import('./$types').Actions} */
export const actions = {
	default: async (event) => {
	login: async (event) => {
		// TODO log the user in
	},
	register: async (event) => {
		// TODO register the user
	}
};

名前付きの action (named action) を呼び出すには、クエリパラメータに / を接頭辞に付与したその action の名前を追加します:

src/routes/login/+page.svelte
<form method="POST" action="?/register">
src/routes/+layout.svelte
<form method="POST" action="/login?/register">

action 属性と同じように、button の formaction 属性を使用することができ、こうすると親の <form> とは別の action に同じ form のデータを POST することができます:

src/routes/login/+page.svelte
<form method="POST">
<form method="POST" action="?/login">
	<label>
		Email
		<input name="email" type="email">
	</label>
	<label>
		Password
		<input name="password" type="password">
	</label>
	<button>Log in</button>
	<button formaction="?/register">Register</button>
</form>

名前付き action (named action) の隣にデフォルトの action を置くことはできません。なぜなら リダイレクト無しで名前付き action (named action) に POST をすると、クエリパラメータが URL に保持され、それ以降デフォルトの POST をしようとしても以前 POST した名前付き action (named action) を通ってしまうからです。

action の解剖学

action はそれぞれ RequestEvent オブジェクトを受け取って、request.formData() でデータを読み込むことができます。リクエスト (例えば、cookie をセットしてユーザーをログインさせるなど) を処理したあと、action は次の更新まで、対応するページでは form プロパティで、アプリ全体では $page.form で利用可能なデータで応答することができます。

src/routes/login/+page.server.js
ts
/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies }) {
Cannot find name 'db'.2304Cannot find name 'db'.
const user = await db.getUserFromSession(cookies.get('sessionid'));
Cannot find name 'db'.2304Cannot find name 'db'.
return { user };
}
/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
const user = await db.getUser(email);
cookies.set('sessionid', await db.createSession(user), { path: '/' });
return { success: true };
},
register: async (event) => {
// TODO register the user
}
};
src/routes/login/+page.server.ts
ts
import type { PageServerLoad, Actions } from './$types';
Cannot find name 'db'.2304Cannot find name 'db'.
export const load: PageServerLoad = async ({ cookies }) => {
Cannot find name 'db'.2304Cannot find name 'db'.
const user = await db.getUserFromSession(cookies.get('sessionid'));
return { user };
};
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
const user = await db.getUser(email);
cookies.set('sessionid', await db.createSession(user), { path: '/' });
return { success: true };
},
register: async (event) => {
// TODO register the user
},
} satisfies Actions;
src/routes/login/+page.svelte
<script>
	/** @type {import('./$types').PageData} */
	export let data;

	/** @type {import('./$types').ActionData} */
	export let form;
</script>

{#if form?.success}
	<!-- このメッセージは一時的なものです; form 送信に対するレスポンスとしてページがレンダリングされたため、存在しています。
		   ユーザーがリロードすると消えます。 -->
	<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}
src/routes/login/+page.svelte
<script lang="ts">
	import type { PageData, ActionData } from './$types';
	
	export let data: PageData;
	
	export let form: ActionData;
</script>

{#if form?.success}
	<!-- このメッセージは一時的なものです; form 送信に対するレスポンスとしてページがレンダリングされたため、存在しています。
		   ユーザーがリロードすると消えます。 -->
	<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}

Validation errors

無効なデータが原因でリクエストが処理できなかった場合、再試行できるようにするために、直前に送信した form の値とともに validation error をユーザーに返すことができます。fail 関数は、HTTP ステータスコード (通常、validation error の場合は 400 か 422) をデータとともに返します。ステータスコードは $page.status から使用することができ、data は form から使用することができます:

src/routes/login/+page.server.js
import { fail } from '@sveltejs/kit';

/** @type {import('./$types').Actions} */
export const actions = {
	login: async ({ cookies, request }) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');

		if (!email) {
			return fail(400, { email, missing: true });
		}

		const user = await db.getUser(email);

		if (!user || user.password !== hash(password)) {
			return fail(400, { email, incorrect: true });
		}

		cookies.set('sessionid', await db.createSession(user), { path: '/' });

		return { success: true };
	},
	register: async (event) => {
		// TODO register the user
	}
};

念のため、password は返さず、email のみをページに返していることにご注意ください。

src/routes/login/+page.svelte
<form method="POST" action="?/login">
	{#if form?.missing}<p class="error">The email field is required</p>{/if}
	{#if form?.incorrect}<p class="error">Invalid credentials!</p>{/if}
	<label>
		Email
		<input name="email" type="email">
		<input name="email" type="email" value={form?.email ?? ''}>
	</label>
	<label>
		Password
		<input name="password" type="password">
	</label>
	<button>Log in</button>
	<button formaction="?/register">Register</button>
</form>

戻り値は JSON としてシリアライズ可能でなければなりません。その上で、構造は完全にあなた次第です。例えば、もしページに複数の form がある場合、返された form データがどの <form> を参照しているかを id プロパティなどで区別することができます。

Redirects

redirect (と error) は load のそれと同じように機能します:

src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';

/** @type {import('./$types').Actions} */
export const actions = {
	login: async ({ cookies, request, url }) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');

		const user = await db.getUser(email);
		if (!user) {
			return fail(400, { email, missing: true });
		}

		if (user.password !== hash(password)) {
			return fail(400, { email, incorrect: true });
		}

		cookies.set('sessionid', await db.createSession(user), { path: '/' });

		if (url.searchParams.has('redirectTo')) {
			redirect(303, url.searchParams.get('redirectTo'));
		}

		return { success: true };
	},
	register: async (event) => {
		// TODO register the user
	}
};

Loading data

action の実行後、そのページは (リダイレクトや予期せぬエラーが発生しない限り) 再レンダリングされ、action の戻り値が form プロパティとしてそのページで使用できるようになります。つまり、ページの load 関数は、action が完了したあとに実行されるということです。

handle は action が呼び出される前に実行され、load 関数より前に再実行されることはないことに注意してください。つまり、例えば handle を使用して cookie を元に event.locals に値を入れる場合、action で cookie を設定したり削除したりするときは event.locals を更新しなければなりません:

src/hooks.server.js
ts
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
}
src/hooks.server.ts
ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
};
src/routes/account/+page.server.js
ts
/** @type {import('./$types').PageServerLoad} */
export function load(event) {
return {
user: event.locals.user
};
}
/** @type {import('./$types').Actions} */
export const actions = {
logout: async (event) => {
event.cookies.delete('sessionid', { path: '/' });
event.locals.user = null;
}
};
src/routes/account/+page.server.ts
ts
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = (event) => {
return {
user: event.locals.user,
};
};
export const actions = {
logout: async (event) => {
event.cookies.delete('sessionid', { path: '/' });
event.locals.user = null;
},
} satisfies Actions;

Progressive enhancement

前のセクションでは クライアントサイドの JavaScriptなしで動作する /login action を構築しました — fetch は見当たりません。これは素晴らしいことですが、JavaScript が利用可能な場合は、より良いユーザーエクスペリンスを提供するために form のインタラクションをプログレッシブに強化 (progressively enhance) することができます。

use:enhance

form をプログレッシブに強化する最も簡単な方法は、use:enhance action を追加することです:

src/routes/login/+page.svelte
<script>
	import { enhance } from '$app/forms';

	/** @type {import('./$types').ActionData} */
	export let form;
</script>

<form method="POST" use:enhance>

ええ、enhance action と <form action> をどちらも 'action' と呼んでいて、少し紛らわしいですよね。このドキュメントは action でいっぱいです。申し訳ありません。

引数が無い場合、use:enhance は、ブラウザネイティブの動作を、フルページリロードを除いてエミュレートします。それは:

  • action が送信元のページと同じ場所にある場合に限り、成功レスポンスまたは不正なレスポンスに応じて、form プロパティと $page.form$page.status を更新します。例えば、<form action="/somewhere/else" ..> というようなフォームの場合、form$page は更新されません。これは、ネイティブのフォーム送信では action があるページにリダイレクトされるからです。どちらにしても更新させたい場合は、applyAction を使用してください
  • <form> 要素をリセットします
  • 成功レスポンスの場合は invalidateAll で全てのデータを無効化・最新化(invalidate)します
  • リダイレクトレスポンスの場合は goto を呼び出します
  • エラーが発生した場合はもっとも近くにある +error 境界をレンダリングします
  • 適切な要素に フォーカスをリセット します

use:enhance をカスタマイズする

この挙動をカスタマイズするために、form が送信される直前に実行される SubmitFunction 関数を提供することができます。そして (オプションで) ActionResult を引数に取るコールバックを返すことができます。もしコールバックを返す場合、上述のデフォルトの動作はトリガーされません。元に戻すには、update を呼び出してください。

<form
	method="POST"
	use:enhance={({ formElement, formData, action, cancel, submitter }) => {
		// `formElement` はこの `<form>` 要素です
		// `formData` は送信される予定の `FormData` オブジェクトです
		// `action` はフォームが POST される URL です
		// `cancel()` を呼び出すと送信(submission)を中止します
		// `submitter` は、フォームの送信を実行した `HTMLElement` です

		return async ({ result, update }) => {
			// `result` は `ActionResult` オブジェクトです
			// `update` は、このコールバックが設定されていない場合に起動されるデフォルトのロジックを起動する関数です
		};
	}}
>

これらの関数を、ロード中の UI (loading UI) を表示したり隠したりすることなどに使用できます。

コールバックを返す場合は use:enhance のデフォルトの動作の一部を再現させる必要があるかもしれませんが、成功レスポンスで全てのデータを無効化・最新化(invalidate)する必要はありません。このような場合には applyAction を使用するとよいでしょう:

src/routes/login/+page.svelte
<script>
	import { enhance, applyAction } from '$app/forms';

	/** @type {import('./$types').ActionData} */
	export let form;
</script>

<form
	method="POST"
	use:enhance={({ formElement, formData, action, cancel }) => {

		return async ({ result }) => {
			// `result` は `ActionResult` オブジェクトです
			if (result.type === 'redirect') {
				goto(result.location);
			} else {
				await applyAction(result);
			}
		};
	}}
>

applyAction(result) の挙動は result.type に依存しています:

  • success, failure$page.statusresult.status に設定し、form$page.formresult.data で更新します (enhanceupdate とは対照的に、送信元がどこかは関係ありません)
  • redirectgoto(result.location, { invalidateAll: true }) を呼び出します
  • error — もっとも近くにある +error 境界を result.error でレンダリングします

いずれの場合でも、フォーカスはリセットされます

Custom event listener

use:enhance ではなく、<form> の通常のイベントリスナーを使うことで、ご自身でプログレッシブ・エンハンスメント(progressive enhancement)を実装することもできます:

src/routes/login/+page.svelte
<script>
	import { invalidateAll, goto } from '$app/navigation';
	import { applyAction, deserialize } from '$app/forms';

	/** @type {import('./$types').ActionData} */
	export let form;

	/** @type {any} */
	let error;

	/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
	async function handleSubmit(event) {
		const data = new FormData(event.currentTarget);

		const response = await fetch(event.currentTarget.action, {
			method: 'POST',
			body: data
		});

		/** @type {import('@sveltejs/kit').ActionResult} */
		const result = deserialize(await response.text());

		if (result.type === 'success') {
			// rerun all `load` functions, following the successful update
			await invalidateAll();
		}

		applyAction(result);
	}
</script>

<form method="POST" on:submit|preventDefault={handleSubmit}>
	<!-- content -->
</form>
src/routes/login/+page.svelte
<script lang="ts">
	import { invalidateAll, goto } from '$app/navigation';
	import { applyAction, deserialize } from '$app/forms';
	
	import type { ActionData } from './$types';
	import type { ActionResult } from '@sveltejs/kit';
	
	export let form: ActionData;
	
	let error: any;
	
	async function handleSubmit(event: { currentTarget: EventTarget & HTMLFormElement }) {
		const data = new FormData(event.currentTarget);
	
		const response = await fetch(event.currentTarget.action, {
			method: 'POST',
			body: data,
		});
	
		const result: ActionResult = deserialize(await response.text());
	
		if (result.type === 'success') {
			// rerun all `load` functions, following the successful update
			await invalidateAll();
		}
	
		applyAction(result);
	}
</script>

<form method="POST" on:submit|preventDefault={handleSubmit}>
	<!-- content -->
</form>

処理を進める前に、$app/formsdeserialize でレスポンスをデシリアライズする必要があることにご注意ください。JSON.parse() では不十分です。なぜなら、例えば load 関数のような form action は、DateBigInt オブジェクトも戻り値としてサポートしているからです。

もし +page.server.js+server.js のどちらも存在する場合、デフォルトでは、fetch リクエストは +server.js のほうにルーティングされます。+page.server.js の action に POST をするには、カスタムの x-sveltekit-action ヘッダーを使用します:

const response = await fetch(this.action, {
	method: 'POST',
	body: data,
	headers: {
		'x-sveltekit-action': 'true'
	}
});

Alternatives

サーバーにデータを送信する方法として、プログレッシブな強化(progressively enhance)を行うことができるため Form actions は望ましい方法ですが、+server.js ファイルを使用して (例えば) JSON API を公開することもできます。それは例えばこのように行います:

send-message/+page.svelte
<script>
	function rerun() {
		fetch('/api/ci', {
			method: 'POST'
		});
	}
</script>

<button on:click={rerun}>Rerun CI</button>
send-message/+page.svelte
<script lang="ts">
	function rerun() {
		fetch('/api/ci', {
			method: 'POST',
		});
	}
</script>

<button on:click={rerun}>Rerun CI</button>
api/ci/+server.js
ts
/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something
}
api/ci/+server.ts
ts
import type { RequestHandler } from './$types';
export const POST: RequestHandler = () => {
// do something
};

GET vs POST

これまで見てきたように、フォームアクションを使うには、method="POST" を使用する必要があります。

サーバーにデータを POST する必要がないフォームもあるでしょう — 例えば検索入力(search inputs)です。これに対応するには method="GET" (または、method を全く書かないのも同等です) を使うことができ、そして SvelteKit はそれを <a> 要素のように扱い、フルページナビゲーションの代わりにクライアントサイドルーターを使用します。:

<form action="/search">
	<label>
		Search
		<input name="q">
	</label>
</form>

この form を送信すると /search?q=... に移動して load 関数が実行されますが、action は実行されません。<a> 要素と同じように、data-sveltekit-reload 属性、 data-sveltekit-replacestate 属性、data-sveltekit-keepfocus 属性、 data-sveltekit-noscroll 属性を <form> に設定することができ、ルーターの挙動をコントロールすることができます。

その他の参考資料