You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 08:31:21