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

React多步骤表单Continue按钮需双击跳转问题求解

React多步骤表单点击Continue需双击跳转的问题解决

问题描述

开发React多步骤表单时,通过Continue和Back按钮切换步骤,遇到以下问题:所有必填字段填写完成后,点击Continue按钮仍需双击才能跳转到下一步。调试发现Main.tsx的handleContinue函数中的return语句是关键:注释该语句时,即使表单有错误也能跳转;保留该语句时,验证通过也无法正常跳转。需要实现必填项完成后单击Continue即可跳转,同时保留表单错误时阻止跳转的逻辑。

问题根源

  1. 状态更新异步性:点击Continue时,sethasAttempted1Continue(true)触发Step1的useEffect执行验证,但isStep1Valid的更新是异步的,第一次点击时该状态还是旧值(false),导致return阻止跳转;第二次点击时状态才更新为true,才能正常跳转。
  2. 逻辑嵌套错误:Main.tsx的handleContinue函数中,currentStep === 2的判断嵌套在currentStep === 1的分支里,导致跳转逻辑完全无法执行。

解决方案

核心思路

让子组件暴露同步验证方法,父组件点击Continue时直接调用该方法获取验证结果,避免依赖异步状态更新判断是否跳转,同时修复逻辑结构错误。

修正后的代码

Step1.tsx(子组件)

import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';

interface Step1Props {
  setErrorMessage: (message: string) => void;
}

interface FormData {
  location?: string;
  title?: string;
  category?: string;
}

const Step1Content: React.FC<Step1Props> = ({
  setErrorMessage,
}) => {
  const [formData, setFormData] = useState<FormData>({});
  const [errors, setErrors] = useState<{ [key: string]: string }>({});

  // 同步验证方法,直接返回验证结果
  const validate = () => {
    const newErrors: { [key: string]: string } = {};
    if (!formData.title) newErrors.title = "Campaign title is required.";
    if (!formData.location) newErrors.location = "Location is required.";
    if (!formData.category) newErrors.category = "Category is required.";

    setErrors(newErrors);
    const isValid = Object.keys(newErrors).length === 0;

    if (!isValid) {
      setErrorMessage(Object.values(newErrors).join(", "));
    } else {
      setErrorMessage("");
    }
    return isValid;
  };

  // 输入变化时实时验证(可选)
  useEffect(() => {
    if (Object.keys(formData).length > 0) {
      validate();
    }
  }, [formData]);

  // 输入处理方法
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
  };

  const handleLocationChange = (value: string) => {
    setFormData(prev => ({ ...prev, location: value }));
  };

  const handleCategoryChange = (value: string) => {
    setFormData(prev => ({ ...prev, category: value }));
  };

  return (
    <div className="flex flex-col gap-y-[30px] pb-[10px] pt-[10px]">
      <CardButtonInput
        placeholder={t("title")}
        type="text"
        name="title"
        value={formData.title}
        onChange={handleInputChange}
        error={errors["title"]}
      />
      <Radio
        options={cities}
        name="location"
        placeholder={t("location")}
        value={formData.location}
        onChange={handleLocationChange}
        error={errors["location"]}
        float={true}
      />
      <Radio
        options={campaignCategory}
        name="category"
        placeholder="Category"
        value={formData.category}
        onChange={handleCategoryChange}
        error={errors["category"]} {/* 修正原拼写错误:cateory → category */}
        float={true}
      />
    </div>
  )
};

// 用forwardRef暴露validate方法给父组件
const Step1 = forwardRef((props: Step1Props, ref) => {
  const stepRef = useRef<{ validate: () => boolean }>(null);
  stepRef.current = { validate: Step1Content.prototype.validate };
  useImperativeHandle(ref, () => stepRef.current!);
  return <Step1Content {...props} />;
});

export default Step1;

Main.tsx(父组件)

import React, { useState, useRef } from 'react';
import Step1 from './Step1';
import Step2 from './Step2';

const Main = () => {
  const [currentStep, setCurrentStep] = useState(1);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const step1Ref = useRef<{ validate: () => boolean }>(null);
  const step2Ref = useRef<{ validate: () => boolean }>(null);

  const handleContinue = () => {
    let isValid = true;

    // 步骤1验证
    if (currentStep === 1) {
      isValid = step1Ref.current?.validate() ?? false;
      if (!isValid) {
        setErrorMessage("Please complete the information before continuing..");
        return;
      }
    }
    // 步骤2验证
    else if (currentStep === 2) {
      isValid = step2Ref.current?.validate() ?? false;
      if (!isValid) {
        setErrorMessage("Please complete the information before continuing..");
        return;
      }
      // 最后一步执行API提交
      try {
        // CallAPI() + payload
      } catch (error) {
        setErrorMessage("Failed to submit the form. Please try again.");
        return;
      }
    }

    // 验证通过,跳转步骤
    setCurrentStep((prevStep) => Math.min(prevStep + 1, 4)); // 假设总共有4步
    window.scrollTo({ top: 0, behavior: "smooth" });
    setErrorMessage(null);
  };

  const handleBack = () => {
    setCurrentStep((prev) => Math.max(prev - 1, 1));
    window.scrollTo({ top: 0, behavior: "smooth" });
  };

  const getStepItems = () => {
    const commonSteps = [
      {
        statusDefault: "default",
        statusVerified: "verified",
        statusUnverified: "unverified",
        personalInformation: t("step1"),
        numberOfSteps: 1,
      },
      {
        statusDefault: "default",
        statusVerified: "verified",
        statusUnverified: "unverified",
        personalInformation: t("step2"),
        numberOfSteps: 2,
      },
      // 其他步骤...
    ];

    return commonSteps.map((step, index) => {
      if (index + 1 < currentStep) return { ...step, status: "verified" };
      else if (index + 1 === currentStep) return { ...step, status: "unverified" };
      return { ...step, status: "" };
    });
  };

  const StepItems = getStepItems();

  return (
    <div>
      <div className="flex flex-row items-center justify-center">
        {StepItems.map((card, index) => (
          <React.Fragment key={index}>
            <CardStep
              status={card.status}
              statusDefault={card.statusDefault}
              statusVerified={card.statusVerified}
              statusUnverified={card.statusUnverified}
              personalInformation={card.personalInformation}
              numberOfSteps={card.numberOfSteps}
            />
            {index < StepItems.length - 1 && (
              <div className="mb-20 flex flex-row items-center justify-center">
                <div className="h-[5px] w-[5px] rounded-full"></div>
                <div className="w-[150px] border-t border-[#9F76FF]"></div>
                <div className="h-[5px] w-[5px] rounded-full"></div>
              </div>
            )}
          </React.Fragment>
        ))}
      </div>

      {currentStep === 1 && (
        <Step1
          ref={step1Ref}
          setErrorMessage={setErrorMessage}
        />
      )}
      {currentStep === 2 && (
        <Step2
          ref={step2Ref}
          setErrorMessage={setErrorMessage}
        />
      )}
      {currentStep === 3 && <Step3 />}
      {currentStep === 4 && <Step4 />}

      <div className="flex flex-row items-center justify-center gap-x-[20px]">
        <CardButton
          text={t("back")}
          border="border-[#9F76FF]"
          color="text-[#9F76FF]"
          bg="bg-transparent"
          onClick={handleBack}
        />
        <CardButton
          text={t("continue")}
          onClick={handleContinue}
        />
      </div>
    </div>
  )
};

export default Main;

关键修改点

  1. 子组件重构:移除hasAttempted1Continue依赖,新增同步validate方法,通过forwardRef暴露给父组件,避免异步状态更新延迟。
  2. 父组件逻辑修复:
    • 移除isStep1Valid、hasAttempted1Continue等冗余状态
    • 修复步骤判断的嵌套错误,改为平级逻辑
    • 点击Continue时直接调用子组件的validate方法,同步获取验证结果,决定是否跳转
  3. 细节修正:修复Step1中Radio组件的错误拼写cateory为category

内容的提问来源于stack exchange,提问作者nikita

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 13:54:57