ข้ามไปยังเนื้อหา

Forms & Actions

SvelteKit จัดการการ submit form ด้วย actions — server function ใน +page.server.js ที่รับข้อมูลที่ส่งมา form HTML ธรรมดา <form method="POST"> จะ post ไปหา action นั้น ฟีเจอร์นี้จึงทำงานได้ ก่อน JavaScript จะโหลดด้วยซ้ำ

src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
/** @param {FormDataEntryValue} email @param {FormDataEntryValue} password @returns {Promise<{ token: string } | null>} */
async function authenticate(email, password) {
return null; // stand-in for a real lookup against your user store
}
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email || !password) {
// Return a 400 with the values so the form can re-render the error.
return fail(400, { email, missing: true });
}
const user = await authenticate(email, password);
if (!user) return fail(401, { email, incorrect: true });
cookies.set('session', user.token, { path: '/' });
redirect(303, '/dashboard'); // throw a redirect on success
},
};
src/routes/login/+page.svelte
<script>
let { form } = $props(); // the action's returned value (e.g. fail data)
</script>
<form method="POST">
<input name="email" value={form?.email ?? ''} />
<input name="password" type="password" />
{#if form?.incorrect}<p>Wrong email or password</p>{/if}
<button>Log in</button>
</form>

ค่าที่ action return (รวมถึงข้อมูลจาก fail(...)) มาถึงเป็น form prop ตอนสำเร็จคุณ redirect ตอนล้มเหลวคุณ fail(status, data) เพื่อให้เพจ re-render พร้อม error และเก็บ input ของผู้ใช้ไว้

หนึ่งเพจมีได้หลาย action — เช่น form login กับ form register ตั้งชื่อให้แต่ละ action แล้วเจาะไปที่ตัวใดตัวหนึ่งด้วย action="?/name":

export const actions = {
login: async ({ request }) => { /* … */ },
register: async ({ request }) => { /* … */ },
};
<form method="POST" action="?/login">…</form>
<form method="POST" action="?/register">…</form>

form ข้างบนทำงานได้อยู่แล้วโดย ไม่มี JavaScript เลย — browser post, server ตอบ, browser ทำตาม redirect เอง พิสูจน์ด้วย curl (ไม่มี browser ไม่มี JS, Accept header หน้าตาแบบ navigation จริงของ browser) กับแอปที่ build จริง:

$ curl -s -D - -o /dev/null -X POST http://localhost:5196/login \
-H "Origin: http://localhost:5196" \
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" \
HTTP/1.1 303 See Other
location: /dashboard
$ curl -s -D - -o /dev/null -X POST http://localhost:5196/login \
-H "Origin: http://localhost:5196" \
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" \
-d "email="
HTTP/1.1 400 Bad Request
content-type: text/html

ไม่มี JavaScript รันเลยสักบรรทัดใน request นั้น — ได้ 303 redirect จริงตอนสำเร็จ และ 400 พร้อม HTML เต็มหน้าที่ re-render (พร้อมข้อมูลจาก fail() เป็น form prop) ตอนล้มเหลว นี่คือประเด็นทั้งหมดของการสร้างบน <form method="POST">: กลไกคือ HTTP มาตรฐาน ไม่ใช่ฟีเจอร์ของ client-side framework

use:enhance เฉย ๆ (ไม่มี argument) ยกระดับ form ให้ submit แบบ AJAX ที่ลื่นด้วยค่า default ที่สมเหตุสมผล การส่ง function เข้าไปทำให้คุณ customize สิ่งที่เกิดขึ้นรอบ ๆ การ submit ได้ — function นั้นรัน ก่อน request และ return callback ที่รัน หลัง response ได้ โดยรับ result กับ update() ที่คุณควบคุมเอง:

src/routes/login/+page.svelte
<script lang="ts">
import { enhance, applyAction } from '$app/forms';
let { form } = $props();
let submitting = $state(false);
function customEnhance() {
submitting = true;
return async ({ result, update }: { result: any; update: () => Promise<void> }) => {
submitting = false;
if (result.type === 'redirect') {
await applyAction(result); // apply the redirect ourselves
} else {
await update(); // default behavior: update form prop, re-run load
}
};
}
</script>
<form method="POST" use:enhance={customEnhance}>
<input name="email" value={form?.email ?? ''} />
<button>Log in</button>
</form>
{#if submitting}<p>submitting…</p>{/if}

update() คือสิ่งเดียวกับที่ use:enhance เฉย ๆ ทำให้อัตโนมัติ: มัน update form prop และรัน load ซ้ำ (เทียบเท่า invalidateAll()) applyAction(result) apply ผล redirect/error/success ที่ return มาแบบเดียวกับที่ default handler ของ SvelteKit จะทำ — มีประโยชน์เมื่อคุณ branch ตาม result.type เองสำหรับบางกรณี แล้วอยากได้ default behavior สำหรับกรณีอื่น svelte-check ยืนยันว่าโค้ดนี้ compile สะอาดตรงกับ type ของ SvelteKit ที่ติดตั้งไว้ (ตรวจกับ 2.70.3)

flowchart TB
  nojs["ไม่มี JS: browser POST form"] --> server["action รันบน server"]
  js["มี use:enhance: AJAX submit ไม่ reload"] --> server
  server --> result["สำเร็จ redirect หรือ fail data ไป form prop"]
progressive enhancement: ทำงานได้ไม่มี JS ดีขึ้นเมื่อมี

นี่คือลายเซ็นของ SvelteKit: สร้างด้วย form ธรรมดาที่ทำงานได้ทุกที่ แล้วค่อย enhance เพื่อประสบการณ์ที่ดีขึ้น — แทนที่จะเป็น form ที่ใช้ JS อย่างเดียวและพังเมื่อไม่มี JS

SvelteKit กำลังพัฒนาทางเลือกใหม่แทน load/actions/+server.js สำหรับการสื่อสารกับ server: remote functions import จาก $app/server ในไฟล์ชื่อ *.remote.ts (หรือ .js) มีสี่แบบ: query (อ่าน), form (mutation ที่ผูกกับ form), command (mutation แบบเรียกตรง ๆ), และ prerender (query ที่ resolve ตอน build time)

// vite.config.js — enabling the flag (flattened kit options passed to the plugin)
import { sveltekit } from '@sveltejs/kit/vite';
export default {
plugins: [sveltekit({ experimental: { remoteFunctions: true } })],
};
// src/lib/posts.remote.js — experimental: requires kit.experimental.remoteFunctions
import { query, command } from '$app/server';
let posts = ['first post', 'second post'];
export const getPosts = query(() => posts);
export const addPost = command('unchecked', (title) => {
posts = [...posts, title];
return { ok: true };
});

เรียกจาก component getPosts() ทำตัวเหมือน reactive query (fetch ซ้ำเมื่อถูก invalidate) และ addPost(title) ทำตัวเหมือน mutation ที่เรียกได้ตรง ๆ — ไม่ต้องมี route file, ไม่ต้อง fetch, ไม่ต้องต่อสาย +server.js endpoint เอง โค้ดนี้ compile และ type-check สะอาด (svelte-check, 0 error) กับ Kit 2.70.3 ตอนเปิด flag แล้ว — แต่ด้วยคำว่า “not yet stable” ที่เห็นข้างบนตรง ๆ ให้มองว่านี่คือตัวอย่างล่วงหน้าของทิศทาง data fetching ในอนาคต ไม่ใช่สิ่งที่ควรเอาไป build production code วันนี้

เวลารีวิวโค้ด form/action ที่ generate มา เช็คสิ่งเหล่านี้:

  • action return plain object ตอน validation ล้มเหลว (return { error: 'bad' }) แทน fail(status, data) หรือเปล่า? plain object หน้าตาเหมือนทำงานได้ (มันไปอยู่ใน form prop ทั้งสองแบบเหมือนกัน) แต่ response status ยังเป็น 200 ซึ่งทำให้อะไรก็ตามที่ branch ตาม HTTP status พัง — และข้าม typing ActionFailure ของ SvelteKit เองไปด้วย
  • throw redirect()/error() ข้างใน try/catch ที่ตั้งใจจับอย่างอื่น ทำให้ control-flow throw ถูกกลืนแบบเงียบ ๆ หรือเปล่า? (พิสูจน์บั๊กนี้เต็ม ๆ พร้อม fix ที่ยืนยันด้วย curl อยู่ใน บท hooks)
  • use:enhance callback ลืมเรียก update() หรือ applyAction() ในบาง branch ทำให้ form prop ค้างข้อมูลเก่าหลัง response บางแบบหรือเปล่า?
  • ปฏิบัติกับ remote functions เหมือนเป็น API ที่เสถียรและใช้ได้เสมอ โดยไม่เช็คว่า kit.experimental.remoteFunctions เปิดอยู่หรือเปล่า หรือไม่บอกผู้อ่านว่าโค้ดนี้ยัง experimental หรือเปล่า?
  • มีสมมติฐานแบบไม่มี matcher ว่า form จะ submit โดยเปิด JavaScript เสมอ — ไม่มี fallback แบบ <form> ธรรมดาเลยหรือเปล่า?

🐛 หา bug ในโค้ดนี้

src/routes/settings/+page.server.js
import { redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const name = data.get('name');
if (!name) {
return { error: 'name is required' }; // looks like a validation error...
}
redirect(303, '/settings');
},
};
เฉลย

โค้ดนี้ return plain object ({ error: '...' }) ตอนล้มเหลว แทนที่จะเป็น fail(400, { error: '...' }) มันยังไปอยู่ใน form prop เหมือนเดิม — {#if form?.error} ในเพจจะ render ได้ปกติ — บั๊กนี้จึงพลาดตาได้ง่ายเวลาอ่านเร็ว ๆ แต่ HTTP response status ยังเป็น 200 OK แทนที่จะเป็น 400 ที่ validation failure ควรมี ซึ่งทำให้โค้ดที่ branch ตาม status (analytics, retry policy, การเช็ค result.type ของ use:enhance เองที่คาดหวัง "failure" เฉพาะจาก fail() จริง ๆ) พัง ทางแก้คือ return fail(400, { error: 'name is required' }) — shape ใน form prop เหมือนเดิม แต่ HTTP semantics ข้างใต้ถูกต้อง

action คือ HTTP form handler ธรรมดาก่อน แล้วค่อยเป็นเวอร์ชัน enhance ด้วย JavaScript ทีหลัง ทุกการตัดสินใจในบทนี้ตามลำดับนั้น: fail() return ข้อมูล ที่เพจ re-render ด้วย (ไม่ใช่ throw error ซึ่งจะโชว์หน้า error ของ framework แทน form); redirect() คือ HTTP redirect จริงที่ browser ทำตามได้โดยไม่มี JavaScript เลย; use:enhance เป็นแค่ การยกระดับ ที่ซ้อนทับบน behavior ที่ทำงานได้อยู่แล้ว remote functions เป็น mental model คนละแบบไปเลย — ใกล้เคียงกับการเรียก function มากกว่า submit form — และเพราะมันบอกตัวเองชัดว่ายัง experimental จึงควรรู้จักไว้ แต่ยังไม่ควรเอาไป build ทับโดยไม่เช็คสถานะมันอีกที

ได้อะไรประโยชน์ต้นทุน
<form method="POST"> ธรรมดา ไม่มี use:enhanceทำงานได้โดยไม่มี JavaScript เลย พิสูจน์แล้วด้วย curl จริงข้างบนreload ทั้งหน้าทุกครั้งที่ submit
use:enhance (ไม่มี argument)submit แบบ AJAX, form/load update ให้อัตโนมัติคุมสิ่งที่เกิดขึ้นรอบ request ได้น้อยกว่า
use:enhance={fn} (custom callback)คุมเต็มที่ (update(), applyAction(), branch เอง)โค้ดเยอะขึ้น มีโอกาสลืม branch มากขึ้น (พิสูจน์แล้วใน SpotTheBug ข้างบน)
Remote functionsไม่ต้องมี route boilerplate สำหรับอ่าน/เขียนแบบง่าย ๆยัง experimental — “may be changed or removed at any time” ตาม type ของ Kit เอง
  • “return { error: '...' } จาก action เหมือนกับ fail(400, { error: '...' })” พิสูจน์แล้วข้างบน: ทั้งสองไปอยู่ใน form prop เหมือนกัน แต่มีแค่ fail() ที่ตั้ง HTTP status ไม่ใช่ 200 ให้ถูก
  • “use:enhance จำเป็นให้ form ทำงาน” พิสูจน์แล้วข้างบนด้วย curl จริง: <form method="POST"> ธรรมดาได้ 303/400 จริงโดยไม่มี JavaScript เลย use:enhance แค่ทำให้ประสบการณ์ดีขึ้น
  • “remote functions คือของทดแทน load/actions ที่เสถียรพร้อมใช้” type ของ Kit เองบอกตรง ๆ ว่ายัง experimental และเปลี่ยนได้ตลอด — ควรรู้จักไว้ แต่ยังไม่ควรพึ่งพา
  • “403 Forbidden ตอน POST form แปลว่าโค้ดผิด” อาจแปลว่า origin ที่ deploy ไม่ได้ตั้งให้ adapter-node ถูกต้องก็ได้ (พิสูจน์แล้วข้างบน) — เป็นปัญหา deployment ไม่ใช่บั๊กในโค้ด
form actions ของ SvelteKit อยู่ที่ไหน
return { error: 'required' } แบบนี้ผิดตรงไหน
export const actions = {
  default: async ({ request }) => {
    const name = (await request.formData()).get('name');
    if (!name) return { error: 'required' };   // ?
    redirect(303, '/ok');
  },
};
action รายงาน validation error ขณะเก็บ input ของผู้ใช้ไว้ยังไง
curl command นี้ไม่รัน JavaScript เลย response 303 จริงนี้พิสูจน์อะไร
$ curl -s -D - -o /dev/null -X POST http://localhost:5196/login \
  -H "Accept: text/html,..." -d "[email protected]"
HTTP/1.1 303 See Other
location: /dashboard
use:enhance={customFn} ทำอะไรได้ที่ use:enhance เฉย ๆ ทำไม่ได้
อะไรคือความจริงเกี่ยวกับ remote functions ของ SvelteKit (query/form/command/prerender จาก $app/server)