Next.js Server Action 返回校验错误时怎么保留表单状态
来源:17golang原创
时间:2026-09-09 05:15:30 308浏览 收藏
Next.js 的 Server Action 校验失败时,表单不要只返回一条字符串错误。更稳妥的做法是让 Action 返回一个可序列化的状态对象,同时带上 values 和 errors,客户端再用 useActionState 接住它。这样用户输入的姓名和邮箱会在服务端拒绝后继续显示,只需要修改有问题的字段。
核心写法是:服务端从 FormData 读取值,校验失败时原样回传安全的字段值和字段错误;客户端把返回的值绑定回输入框。预期的业务校验不要用 throw,真正的系统异常再交给错误边界。
useActionState的 action 首参是上一轮状态,第二个参数才是提交的FormData。- 失败状态至少保留可回显的
values与字段级errors,不要把密码等敏感字段回传。 - 校验失败返回对象,数据库或网络故障抛出异常并交给 Error Boundary。
一、先把表单状态设计成可回显的数据
Server Action 的返回值会成为下一次渲染的状态,因此结构要简单、稳定、可序列化。下面只回显姓名和邮箱;如果表单有密码、一次性验证码或文件,不要把它们放进返回值。
| 字段 | 用途 | 失败时处理 |
|---|---|---|
| values | 保留用户刚提交的安全字段 | 绑定回 input 的 value |
| errors | 字段级校验信息 | 显示在对应字段下方 |
| message / ok | 表单级结果 | 显示成功或通用提示 |
这个结构把“用户输入”和“服务端意见”分开,后续增加手机号或公司名时,只扩展对应字段,不必让组件猜测一条错误字符串应该放在哪里。

二、校验失败时从 Server Action 回传 values
Server Action 文件放在服务端边界内,先读取字符串,再做长度和格式判断。示例中的 saveProfile 没有把前一次状态当成表单值来源,而是以本次提交的 FormData 为准;previousState 主要用于满足 Action 签名和扩展连续提交状态。
// app/actions/profile.ts
'use server'
type ActionState = {
ok: boolean
values: { name: string; email: string }
errors: { name?: string; email?: string }
message?: string
}
export const initialProfileState: ActionState = {
ok: false,
values: { name: '', email: '' },
errors: {},
}
export async function saveProfile(
previousState: ActionState,
formData: FormData,
): Promise {
// 只读取允许回显的字段,避免把敏感字段带回客户端
const name = String(formData.get('name') ?? '').trim()
const email = String(formData.get('email') ?? '').trim()
const errors: ActionState['errors'] = {}
// 预期的业务校验用返回值表达,不用 throw
if (name.length 0) {
return { ok: false, values: { name, email }, errors }
}
// 生产环境还要在这里检查身份、权限,并执行持久化
await persistProfile({ name, email })
return { ok: true, values: { name, email }, errors: {}, message: '资料已保存' }
}
async function persistProfile(profile: { name: string; email: string }) {
// 示例占位:实际项目在此调用数据库或领域服务
void profile
}
注意第一个参数不能写成 formData。使用 useActionState 后,React 会把上一轮状态放在第一位,把本次提交的 FormData 放在第二位。成功路径可以返回 ok: true,也可以在持久化并刷新数据后重定向;不要在 redirect 后继续依赖返回值。
三、用 useActionState 把返回状态绑定回输入框
客户端组件把返回的 dispatcher 直接交给 form action。关键点是输入框使用 state.values 作为受控值,错误文字使用同一个状态对象;服务端返回失败对象后,组件重新渲染,用户刚输入的内容就不会消失。
// app/profile/profile-form.tsx
'use client'
import { useActionState, useEffect, useState } from 'react'
import { initialProfileState, saveProfile } from '@/app/actions/profile'
export function ProfileForm() {
const [state, formAction, pending] = useActionState(
saveProfile,
initialProfileState,
)
const [fields, setFields] = useState(state.values)
useEffect(() => {
// 只有服务端返回新 values 时回填,用户输入过程不会被覆盖
setFields(state.values)
}, [state.values.name, state.values.email])
return (
)
}
示例把输入值放在本地 fields 中,用户编辑时即时更新;当服务端返回新的 state.values 时,再通过 useEffect 回填。无论选择受控还是非受控方案,name 属性都不能省略,否则字段不会进入 FormData。

四、把校验错误和意外异常分开处理
格式不对、邮箱已存在等可预期结果,应作为普通返回值留在当前表单中;数据库不可用、代码空指针或未处理的网络故障,才应该抛出异常并由路由级 Error Boundary 展示兜底页面。这样用户能修正输入,也不会把内部堆栈暴露到页面。
发布前逐项检查:Server Action 内再次做身份与权限校验;返回对象只含可序列化且允许公开的字段;每个输入保留 name;错误提示有 role="alert" 或等效可访问性语义;提交按钮根据 pending 禁用,避免重复提交。若成功后需要显示新数据,再按页面缓存策略调用 revalidatePath 或其他合适的刷新方法。
相关问题
为什么只返回 errors,输入框还是空了?
因为重新渲染时没有可用的字段值。服务端应从本次 FormData 生成安全的 values,并由客户端绑定回输入框。
校验失败应该 throw 吗?
通常不应该。校验失败是用户可修正的业务结果,返回 ActionState 更适合;真正的系统异常再交给 Error Boundary。
为什么 action 的参数顺序容易写错?
useActionState 会把 reducer action 的首参改为 previousState,FormData 变成第二参。若不使用该 Hook,直接传给 form 的 Server Action 才是单参数 FormData。
-
134 收藏
-
285 收藏
-
356 收藏
-
215 收藏
-
379 收藏
-
154 收藏
-
127 收藏
-
352 收藏
-
381 收藏
-
204 收藏
-
282 收藏
-
286 收藏
-
文章 · 前端 | 9小时前 | javascript · Fetch API · 前端请求 · 异步取消 · AbortController AbortSignal.any AbortSignal.timeout fetch取消请求263 收藏
-
238 收藏
-
168 收藏
-
文章 · 前端 | 13小时前 | javascript · structuredClone · 对象复制 · JavaScript 深拷贝 structuredClone DataCloneError377 收藏
-
349 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习