先记住这个答案
多步提交流程应被视为一个状态机。将currentStep、表单数据和错误信息合并为一个状态对象,由useActionState托管。每次提交触发action,action根据当前步骤解析并验证数据,若通过则更新步骤和数据,否则返回错误。useActionState的状态由action返回值驱动,但需配合pending状态和防重复提交处理(如禁用按钮)以应对客户端并发。通过pending状态和隐藏字段,可实现可靠的步骤导航。
- 用单一状态对象承载步骤、数据与错误
- 在action中完成校验与步骤推进
- useActionState确保状态更新来自服务端
useActionState如何驱动多步状态
useActionState接收一个action函数与初始状态,返回状态与触发函数。在多步表单中,可将整个流程的状态设计为{ step: number, data: object, errors: object }。每次提交时,action函数接收prevState和formData,通过解析prevState.step决定哪些字段属于当前步骤,从而提取数据并进行验证。
验证通过后,action返回{ step: prevState.step+1, data: 合并后的数据, errors: {} },否则返回{ ...prevState, errors }。这样客户端状态只来自action返回值,任何中间操作都通过action触发,但需配合pending状态防止重复提交,以确保状态更新的一致。
三步注册表单的实践
以一个注册流程为例,分三步:基本信息、公司信息、确认。初始状态为{ step:1, data:{}, errors:{} }。每个步骤有自己的表单字段,提交时通过隐藏的step字段或从state获取当前步骤,action据此调用不同的验证逻辑。当第一步提交时,校验姓名和邮箱;通过后返回step:2,客户端渲染第二步并保留已填数据。
当用户点击“上一步”时,可在action中增加intent字段区分前进或后退,从而统一状态更新。这样所有步骤切换都通过action处理,确保数据合并顺序正确,避免因客户端异步修改导致中间数据丢失或重复提交。
状态持久化的边界与风险
useActionState的状态存在客户端内存中,刷新页面会丢失。若用户刷新,需重建状态。通常做法是将当前步骤和已填数据存入URL查询参数或localStorage,但需注意安全性。如果依赖数据库将草稿保存到服务端,则更可靠,但会增加请求次数。
当步骤之间存在动态跳转逻辑(如根据前一步的选择跳过某些步骤)时,action内需包含分支判断,这可能增加复杂度。替代方案是使用客户端状态库如Zustand,但会牺牲服务端校验的严谨性。需权衡易用性与数据一致性,优先保证核心表单的提交可靠。
容易答错的地方
- 每个步骤独立useState
- 有开发者认为每个步骤应有自己的useState保存临时数据,在切换步骤时手动合并。这会脱离服务端控制,异步操作时易产生不一致。应使用一个useActionState管理整个流程,所有状态变更通过action集中处理。
- 在客户端合并数据
- 另一种误区是使用useState存储全部字段,提交时一次性传给服务端。这会使校验延后到最终提交,无法在中间步骤及时反馈错误。正确方式是在每个步骤的action中校验并累积数据,错误通过状态回传。
面试官还会怎么问?
如果用户点击浏览器的刷新按钮,多步表单状态会怎样?
刷新会重置为初始状态,因为useActionState状态在内存中。可通过将步骤和已填数据编码在URL参数中,或用路由前进后退恢复,或提供草稿保存接口。更稳妥的方案是在每次步骤推进时将数据持久化到服务端,刷新后从服务端读取恢复。
多步提交中,如何防止用户绕过某一步直接提交?
action中不应信任客户端传入的step值。必须由服务端维护已完成的步骤,例如在数据库中保存草稿,每次提交时从服务端读取用户当前进度,并校验提交数据所属步骤是否与进度匹配。若数据缺失或步骤跳跃,则拒绝提交。不能仅依赖提交的字段判断,因为客户端可伪造完整数据。
能否用useActionState实现异步验证(如检查用户名唯一)?
可以,action本身是async函数,内部可执行异步请求。验证期间可通过pending状态禁用提交按钮,避免重复触发。返回的状态需包含错误信息,例如用户名已存在,则将错误放入errors对象。注意竞态条件,最好以最新一次请求为准。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。