React JS 发起API请求与页面前置表单校验实现方案
问题原因
你之前的实现里,handleSubmit函数触发后第一时间就启动加载状态、构造请求参数发接口请求,校验逻辑完全没有放在请求、路由跳转逻辑的最前端做拦截,不管用原生判断还是第三方表单库,只要不把校验失败后的中断逻辑放在异步操作之前,都会出现未校验就提交、跳页的问题。
另外原代码还有几个隐性bug会影响校验效果:
- 按钮设置了
type="submit"但没有给form标签绑定提交事件,也没有阻止原生表单默认提交行为,容易出现逻辑冲突 - 联系方式下拉框默认选项没有设置空value,会导致默认选中态判断异常
- 没有错误提示,用户提交失败后不知道哪里不符合要求
- 接口请求没有加异常捕获,请求失败时加载状态会一直转
- 没有对输入值做去空格处理,用户输入全空格可以绕过非空校验
修改后可直接运行的代码
import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import Page from "react-page-loading"; function Checkout({ cart, setResponse }) { const [contactPreference, setContactPreference] = useState(""); const [name, setName] = useState(""); const [phone, setPhone] = useState(""); const [email, setEmail] = useState(""); const [errors, setErrors] = useState({}); const navigate = useNavigate(); const [spinner, setSpinner] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); const newErrors = {}; // 校验逻辑全部放在最前面,不通过直接拦截 if (!name.trim()) newErrors.name = "请输入姓名"; if (!contactPreference) newErrors.contact = "请选择联系方式类型"; if (contactPreference === "phone" && !phone.trim()) { newErrors.phone = "请输入手机号"; } if (contactPreference === "email") { if (!email.trim()) { newErrors.email = "请输入邮箱"; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { newErrors.email = "请输入有效的邮箱地址"; } } // 存在校验错误直接中断,不执行后续请求和跳转 if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } setErrors({}); setSpinner(true); const token = "2c506c6b-d880-36bb-bdba-a035d1464b35"; const data = { transactionReference: "string", paymentMethod: "CreditCard", checkoutOrderUrl: "http://www.test.com/", user: { name: name.trim(), msisdn: contactPreference === "phone" ? "+27" + phone.trim() : "", email: contactPreference === "email" ? email.trim() : "", }, payementMethodDetail: { RedirectUrl: "http://localhost:3000/", PurchaseEventWebhookUrl: "http://www.test.com", }, bundle: cart.map((item) => ({ ProductCode: `${item.ProductCode}`, Amount: item.amount, CurrencyCode: item.currencyCode, Quantity: item.quantity, })), }; const requestOptions = { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(data), }; try { const res = await fetch( "https://api.flash-internal.flash-group.com/ecommerceManagement/1.0.0/api/checkout/", requestOptions ); const result = await res.json(); setResponse(result); setSpinner(false); navigate("/payment"); } catch (err) { setSpinner(false); setErrors({ submit: "提交失败,请稍后重试" }); } }; return ( <div className="App"> <div> <div> Name: <input onChange={(event) => setName(event.target.value)} /> {errors.name && <span style={{color: 'red', marginLeft: 8}}>{errors.name}</span>} </div> <form className="form-inline" onSubmit={handleSubmit}> <label className="my-1 mr-2" htmlFor="inlineFormCustomSelectPref"> How should we contact you ?? </label> <select onChange={(e) => setContactPreference(e.target.value)} className="custom-select my-1 mr-sm-2" id="inlineFormCustomSelectPref" > <option value="" selected>Choose...</option> <option value="phone">Phone Number</option> <option value="email">Email</option> </select> {errors.contact && <span style={{color: 'red'}}>{errors.contact}</span>} {contactPreference === "phone" ? ( <div style={{display: 'inline-block'}}> <input placeholder="+27" onChange={(event) => setPhone(event.target.value)} /> {errors.phone && <span style={{color: 'red', marginLeft: 8}}>{errors.phone}</span>} </div> ) : contactPreference === "email" ? ( <div style={{display: 'inline-block'}}> <input placeholder="Email" onChange={(event) => setEmail(event.target.value)} /> {errors.email && <span style={{color: 'red', marginLeft: 8}}>{errors.email}</span>} </div> ) : null} </form> </div> <button type="submit"> Proceed to Payment </button> {errors.submit && <p style={{color: 'red'}}>{errors.submit}</p>} {spinner && <Page loader={"spin"} color={"#b2fa00"} size={4}></Page>} </div> ); } export default Checkout;
关键改动说明
- 把提交事件绑定到form标签的
onSubmit事件上,移除按钮上单独绑定的onClick,同时通过e.preventDefault()阻止原生表单默认跳转行为,避免逻辑冲突 - 所有校验逻辑全部放在
handleSubmit执行流的最前端,只要存在不符合规则的字段,就更新错误提示并直接return中断函数执行,后续的加载态启动、接口请求、路由跳转全不会触发 - 修正下拉选择框默认选项的value为空字符串,解决选中态判断异常的问题
- 每个字段增加对应的错误提示,校验不通过时直接在对应输入框旁展示错误原因
- 将原有的then链式调用改为async/await + try/catch写法,接口请求失败时可以正常关闭加载状态,避免页面卡死
- 所有入参拼接前先做trim去空格处理,防止用户输入全空格绕过非空校验
- 仅当用户选中对应联系方式时,才向接口传递对应的手机号/邮箱字段,避免提交无效空值
如果后续要替换成Joi、React Hook Form(你提到的React-Form-Hood应为拼写错误)这类第三方表单方案,核心逻辑不变:校验拦截必须放在所有异步操作之前,校验失败时直接中断执行流,不要继续往下走请求和跳转逻辑,之前方案不生效基本都是因为没有做好失败后的流程中断。
内容的提问来源于stack exchange,提问作者Manasseh Codes
相关产品推荐
相关产品推荐

