前端转全栈 08:表单与服务端 Action——渐进增强的提交
系列目录:本文是「前端转全栈:Next.js + Supabase 实战」系列的第 8 篇。前面我们用 Route Handler 写接口,这一篇介绍更贴合 React 的写法——Server Actions,让表单提交变得极简。
前端写表单,习惯 onSubmit 里 fetch 到 /api/xxx。在 Next.js 全栈里,还有一种更顺手的方案:Server Actions——直接在服务端定义函数,表单 action 直接调用它,连 API 路由都不用建。
一、传统 fetch 方案(复习)
"use client"
function CommentForm({ slug }: { slug: string }) {
const [loading, setLoading] = useState(false)
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
const fd = new FormData(e.currentTarget)
await fetch("/api/comments", {
method: "POST",
body: JSON.stringify(Object.fromEntries(fd)),
headers: { "Content-Type": "application/json" },
})
setLoading(false)
}
return <form onSubmit={onSubmit}>…</form>
}
能用,但「建 Route Handler + 手写 fetch + 管理 loading」三步都不少。
二、Server Actions:表单直连服务端函数
// app/blog/[slug]/page.tsx (Server Component)
import { createClient } from "@/lib/supabase/server"
import { revalidatePath } from "next/cache"
async function addComment(formData: FormData) {
"use server" // 标记这是服务端 Action
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error("请先登录")
const content = String(formData.get("content"))
await supabase.from("comments").insert({ content, user_id: user.id, post_slug: /* ... */ })
revalidatePath(`/blog/${/* slug */}`) // 刷新评论区
}
export default function Page() {
return (
<form action={addComment}>
<textarea name="content" required />
<button type="submit">提交</button>
</form>
)
}
注意:
- 函数顶部
"use server"声明它是服务端 Action。 <form action={addComment}>不用onSubmit、不用fetch,浏览器原生就能提交(这就是「渐进增强」——JS 没加载也能用)。- 成功后
revalidatePath让评论区自动更新,无需客户端手动刷新。
三、管理 loading 与错误:useFormStatus / useActionState
Server Action 没有 onSubmit 里的 setLoading,怎么办?用 React 官方 hook:
"use client"
import { useFormStatus } from "react-dom"
function SubmitButton() {
const { pending } = useFormStatus() // 提交中自动为 true
return <button disabled={pending}>{pending ? "提交中…" : "提交"}</button>
}
错误状态用 useActionState(React 19 / Next 15):
"use client"
import { useActionState } from "react"
function CommentForm({ action }: { action: any }) {
const [state, formAction, pending] = useActionState(action, { error: null })
return (
<form action={formAction}>
{state.error && <p className="text-red-500">{state.error}</p>}
<textarea name="content" />
<SubmitButton />
</form>
)
}
服务端 Action 返回 { error: "请先登录" },客户端就能显示出来。
四、两种方案怎么选?
| | Route Handler + fetch | Server Action | |---|---|---| | 适用 | 对外 API、SPA 式交互、第三方调用 | 表单提交、页面内的写操作 | | 渐进增强 | 需额外处理 | 原生支持 | | 代码量 | 多(接口 + 客户端) | 少(一个函数) |
经验:表单提交优先 Server Action;要对外开放的接口才用 Route Handler。
五、别忘了校验仍在服务端
无论哪种方案,校验(第 6 篇的 zod)必须放在服务端。客户端 required、字数限制只是体验优化,恶意请求会绕过它们。Server Action 里照样先用 zod 校验 formData。
总结
- Server Actions:
<form action={服务端函数}>,无需手写 API 路由,支持渐进增强。 useFormStatus管 loading,useActionState管错误返回。- 选择:表单用 Action,对外接口用 Route Handler。
- 校验永远在服务端兜底。
至此,阶段二(Next.js 全栈实战)完成——你已经能用 Next.js + Supabase 写出带登录、能读写数据库的页面了。下一篇进入阶段三,先补最要命的安全实战。
练习:把第 4 篇的
/api/comments提交,改成用 Server Action 实现:建一个addComment(formData)函数,表单action直接调用,提交后revalidatePath刷新评论列表。
Comments
Sign in to leave a comment.