留言板(可运行)
留言存于模块级内存数组,服务重启即失;禁用浏览器 JS 再提交,表单依然可用
提交后 action 里调了 revalidatePath,列表由服务端重新渲染。
Next.js
11-10 01:29:47 UTC
我是种子留言:禁用 JavaScript 再提交一条试试,表单照样工作。
表单三件套的分工
useActionState / useFormStatus / useOptimistic 不是互替品,各管一段时间轴
- useActionState = 结果态:绑定 action 后拿到「上一次提交的返回值」, 驱动报错/成功提示;本页还借「每次派发都换新 state 对象」这一点, 在 effect 里精确捕获成功时刻做 form.reset()
- useFormStatus = 进行态:必须是
<form> 的子孙组件(本页的 SubmitButton), 读父表单本次提交的 pending 来禁用按钮、切换文案 —— 不需要 props 透传, 所以它管不到 form 之外的元素 - useOptimistic = 乐观态:提交即把留言叠上列表(虚线 + 半透明 + 「发送中」), 服务端确认后由 revalidatePath 带回的真值无缝顶替,失败则自动回滚、错误交给 state.error 展示 —— 乐观层只活到所在 transition 结束,实现细节见 GuestbookForm 头注
- 渐进增强与乐观更新的共存:乐观项必须包在 action 闭包里 (与 action 同属一个 transition;在 onSubmit 里另开 startTransition 会 与表单 transition 纠缠死锁),但 SSR 不会为客户端闭包渲染隐藏 action 字段, 无 JS 原生 POST 会打空。解法是 mounted 开关:水合前
<form action>引用 useActionState 的 dispatch(隐藏字段齐全,无 JS 可提交), 水合后切换为乐观闭包 —— 实现细节见 GuestbookForm 头注
三个要点
渐进增强、入参安全、与 Route Handler 的分工
- 渐进增强:
<form action>引用的 action 会被编译成真实的 POST 端点;JS 加载前/被禁用时表单退化为原生提交, 整页刷新但功能完整;JS 可用时升级为无刷新提交 + useActionState 状态反馈 - 永远不要信任入参:action 是公开的网络端点, 请求可以绕过你的表单直接构造;input 上的 required/maxLength 只是体验优化, 非空、长度、权限校验必须在 action 里重做(见 actions.ts)
- 与 Route Handler 分工:站点内部的变更(表单、按钮、乐观更新)用 Server Actions,省去手写端点与 fetch 封装,还自带 revalidatePath 刷新闭环; 对外的 HTTP 界面(第三方、移动端、webhook、需要 GET 语义的资源)用 Route Handler —— 对照本分类外的 /data/route-handlers 专题
什么时候别用 Server Actions
它是「站内变更原语」,不是万能端点
- 需要被浏览器/CDN 缓存的读取:action 只支持 POST 语义,读取交给 Server Component 直取或 GET Route Handler
- 给非本站调用方的 API:外部系统不会带 Next 的 action 协议头,必须 Route Handler
- 大文件上传等长耗时传输:action 默认有请求体上限且不适合流式上传,走专用上传通道(见 next-upload 包)