Forms & Actions
form POST ไปที่ actions
หัวข้อที่มีชื่อว่า “form POST ไปที่ actions”SvelteKit จัดการการ submit form ด้วย actions — server function ใน +page.server.js ที่รับข้อมูลที่ส่งมา form HTML ธรรมดา <form method="POST"> จะ post ไปหา action นั้น ฟีเจอร์นี้จึงทำงานได้ ก่อน JavaScript จะโหลดด้วยซ้ำ
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 },};<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 ของผู้ใช้ไว้
named actions
หัวข้อที่มีชื่อว่า “named actions”หนึ่งเพจมีได้หลาย 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>progressive enhancement พิสูจน์แล้วโดยไม่มี JavaScript เลย
หัวข้อที่มีชื่อว่า “progressive enhancement พิสูจน์แล้วโดยไม่มี JavaScript เลย”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" \ -d "[email protected]"HTTP/1.1 303 See Otherlocation: /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 Requestcontent-type: text/htmlไม่มี JavaScript รันเลยสักบรรทัดใน request นั้น — ได้ 303 redirect จริงตอนสำเร็จ และ 400 พร้อม HTML เต็มหน้าที่ re-render (พร้อมข้อมูลจาก fail() เป็น form prop) ตอนล้มเหลว นี่คือประเด็นทั้งหมดของการสร้างบน <form method="POST">: กลไกคือ HTTP มาตรฐาน ไม่ใช่ฟีเจอร์ของ client-side framework
use:enhance ที่มี custom submit callback
หัวข้อที่มีชื่อว่า “use:enhance ที่มี custom submit callback”use:enhance เฉย ๆ (ไม่มี argument) ยกระดับ form ให้ submit แบบ AJAX ที่ลื่นด้วยค่า default ที่สมเหตุสมผล การส่ง function เข้าไปทำให้คุณ customize สิ่งที่เกิดขึ้นรอบ ๆ การ submit ได้ — function นั้นรัน ก่อน request และ return callback ที่รัน หลัง response ได้ โดยรับ result กับ update() ที่คุณควบคุมเอง:
<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"]
นี่คือลายเซ็นของ SvelteKit: สร้างด้วย form ธรรมดาที่ทำงานได้ทุกที่ แล้วค่อย enhance เพื่อประสบการณ์ที่ดีขึ้น — แทนที่จะเป็น form ที่ใช้ JS อย่างเดียวและพังเมื่อไม่มี JS
Remote functions (experimental): อีกวิธีคุยกับ server
หัวข้อที่มีชื่อว่า “Remote functions (experimental): อีกวิธีคุยกับ server”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.remoteFunctionsimport { 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 วันนี้
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลารีวิวโค้ด form/action ที่ generate มา เช็คสิ่งเหล่านี้:
- action return plain object ตอน validation ล้มเหลว (
return { error: 'bad' }) แทนfail(status, data)หรือเปล่า? plain object หน้าตาเหมือนทำงานได้ (มันไปอยู่ในformprop ทั้งสองแบบเหมือนกัน) แต่ response status ยังเป็น200ซึ่งทำให้อะไรก็ตามที่ branch ตาม HTTP status พัง — และข้าม typingActionFailureของ SvelteKit เองไปด้วย - throw
redirect()/error()ข้างในtry/catchที่ตั้งใจจับอย่างอื่น ทำให้ control-flow throw ถูกกลืนแบบเงียบ ๆ หรือเปล่า? (พิสูจน์บั๊กนี้เต็ม ๆ พร้อม fix ที่ยืนยันด้วยcurlอยู่ใน บท hooks) use:enhancecallback ลืมเรียกupdate()หรือapplyAction()ในบาง branch ทำให้formprop ค้างข้อมูลเก่าหลัง response บางแบบหรือเปล่า?- ปฏิบัติกับ remote functions เหมือนเป็น API ที่เสถียรและใช้ได้เสมอ โดยไม่เช็คว่า
kit.experimental.remoteFunctionsเปิดอยู่หรือเปล่า หรือไม่บอกผู้อ่านว่าโค้ดนี้ยัง experimental หรือเปล่า? - มีสมมติฐานแบบไม่มี
matcherว่า form จะ submit โดยเปิด JavaScript เสมอ — ไม่มี fallback แบบ<form>ธรรมดาเลยหรือเปล่า?
🐛 หา bug ในโค้ดนี้
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 ข้างใต้ถูกต้อง
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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 ทับโดยไม่เช็คสถานะมันอีกที
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
<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 เอง |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “return
{ error: '...' }จาก action เหมือนกับfail(400, { error: '...' })” พิสูจน์แล้วข้างบน: ทั้งสองไปอยู่ในformprop เหมือนกัน แต่มีแค่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 ไม่ใช่บั๊กในโค้ด