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

Next.js页面导航返回后Invisible reCAPTCHA消失问题排查

问题原因

这个问题是Next.js客户端路由跳转特性 + react-google-recaptcha渲染机制共同导致的:

  • Next.js用next/link做跳转时属于单页应用客户端路由切换,不会触发全局页面刷新,Google reCAPTCHA官方脚本只会在页面首次全量加载时扫描DOM、渲染挂载的验证码组件
  • 离开验证码页面时组件被React卸载,reCAPTCHA脚本不会自动重新初始化新挂载的验证码节点,返回页面时就会出现组件空白、无法加载的情况
解决方案

一共两步,修改后即可解决路由跳转返回后验证码消失的问题:

1. 核心修复逻辑

  • 不要静态引入react-google-recaptcha,改用Next.js动态导入并关闭SSR,避免reCAPTCHA依赖浏览器对象导致的服务端渲染初始化异常
  • 给ReCAPTCHA组件绑定动态key,组件每次挂载(路由切回当前页面)时更新key,强制组件重新渲染
  • 组件挂载时检查全局reCAPTCHA实例,主动调用重置方法触发脚本重新识别节点

2. 修改后的Contact页面完整代码

import { useState, useEffect, useRef } from "react";
import { pattern } from "./pricing";
import styles from "./../styles/Contact.module.css";
import { post } from "../helper";
import { CONTACT_PAGE_ACTION_EVENT } from "../constant";
import ClipLoader from "react-spinners/ClipLoader";
// 替换原静态引入,动态导入关闭SSR
import dynamic from 'next/dynamic';
const ReCAPTCHA = dynamic(() => import("react-google-recaptcha"), { ssr: false });


function Contact({captcha_site_key}) {
  const [formData, setFormData] = useState({
    firstName: "",
    email: "",
    emailError: "",
    phone: "",
    message: "",
    responseText: ""
  });
  const recaptchaRef = useRef(null);
  const [disabled, setDisabled] = useState(true);
  const [loading, setLoading] = useState(false)
  // 新增key状态控制reCAPTCHA强制重渲染
  const [recaptchaKey, setRecaptchaKey] = useState(0);

  // 组件每次挂载时触发验证码重初始化
  useEffect(() => {
    setRecaptchaKey(prev => prev + 1);
    if (typeof window !== 'undefined' && window.grecaptcha) {
      window.grecaptcha.ready(() => {
        recaptchaRef.current?.reset();
      });
    }
  }, []);

  const formDataHandler = (name, value) => {
    setFormData((prev) => {
      if (name === "email") {
        return {
          ...prev,
          [name]: value.trimStart(),
          ["emailError"]: pattern.test(value.trimStart())
            ? ""
            : "That is Not a valid email",
        };
      } else {
        return {
          ...prev,
          [name]: value.trimStart(),
        };
      }
    });
  };

  useEffect(() => {
    setDisabled(
      !(
        formData.email.length > 0 &&
        formData.firstName.length > 0 &&
        formData.message.length > 0 &&
        formData.phone.length > 0 &&
        formData.emailError.length === 0
      )
    );
  }, [formData]);
  
  const sendData = async(e) => {
    e.preventDefault();
    setLoading(true)
    const token = await recaptchaRef.current.executeAsync();
    recaptchaRef.current.reset();
    const {emailError, responseText, ...data} = formData;

    // 过滤输入内容的html标签和特殊字符
    const secureData = {
      firstName: data.firstName.replace(/(<([^>]+)>)/gi, "").replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, ''),
      email: data.email.replace(/(<([^>]+)>)/gi, "").replace(/[`~!#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, ''),
      phone: data.phone.replace(/(<([^>])>)/gi, "").replace(/[`~!@#$%^&*()_|\-=?;:'",.<>\{\}\[\]\\\/]/gi, ''),
      message: data.message.replace(/(<([^>]+)>)/gi, "").replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, ''),
      token
    }
    const result = await  post(secureData, CONTACT_PAGE_ACTION_EVENT)

    if(result.status === 'success'){
      setFormData({
        firstName: "",
        email: "",
        emailError: "",
        phone: "",
        message: "",
        responseText: result?.status
      })
    }else {
      setFormData(prev => {
        return {...prev, ['responseText']: result?.status}
      })
    }
    setLoading(false)

  }
  return (
    <section className={styles.contact_container}>
      <ReCAPTCHA
        key={recaptchaKey}
        ref={recaptchaRef}
        size="invisible"
        sitekey={captcha_site_key}
      />
      <section className={styles.contact_section}>
        <form>
          <h3> Let's Talk </h3>
          <p> Fill out the form to send us a message </p>

          {/* 名字输入 */}
          <div className="form-floating mb-3">
            <input 
              className="form-control shadow-none border-dark rounded-0" 
              id="floatingInput" 
              value={formData.firstName}
              style={{ backgroundColor: loading ? '#f1f1f1' : 'white' }}
              disabled={loading}
              required
              type="text"
              name="firstName"
              placeholder="First Name*"
              onChange={(e) =>
                formDataHandler(e.target.name, e.target.value.trimStart())
              }
            />
            <label htmlFor="floatingInput" className="text-muted">First Name*</label>
          </div>

          {/* 邮箱输入 */}
          <div>
            <div className="form-floating">
              <input 
                className="form-control shadow-none border-dark rounded-0" 
                id="floatingInput" 
                value={formData.email}
                style={{ backgroundColor: loading ? '#f1f1f1' : 'white' }}
                disabled={loading}
                required
                type="email"
                name="email"
                placeholder="Email*"
                onChange={(e) =>
                  formDataHandler(e.target.name, e.target.value.trimStart())
                }
              />
              <label htmlFor="floatingInput" className="text-muted">Email*</label>
            </div>
            <p> {formData.emailError} </p>
          </div>
            
          {/* 手机号输入 */}
          <div className="form-floating mb-3">
            <input 
              className="form-control shadow-none border-dark rounded-0" 
              id="floatingInput" 
              value={formData.phone}
              required
              disabled={loading}
              style={{ backgroundColor: loading ? '#f1f1f1' : 'white' }}
              type="text"
              placeholder="Phone Number*"
              name="phone"
              onChange={(e) =>
                formDataHandler(e.target.name, e.target.value.trimStart())
              }
            />
            <label htmlFor="floatingInput" className="text-muted">Phone Number*</label>
          </div>

          {/* 留言输入 */}
          <div className="form-floating mb-3">
            <input 
              className="form-control shadow-none border-dark rounded-0" 
              id="floatingInput" 
              value={formData.message}
              required
              type="text"
              disabled={loading}
              style={{ backgroundColor: loading ? '#f1f1f1' : 'white' }}
              placeholder="Message"
              name="message"
              onChange={(e) =>
                formDataHandler(e.target.name, e.target.value.trimStart())
              }
            />
            <label htmlFor="floatingInput" className="text-muted">Message*</label>
          </div>
          
          {/* 提交按钮 */}
          <button
            disabled={disabled || loading}
            style={{
              display: 'flex',
              justifyContent: 'center',
              alignItems: 'center',
              opacity: disabled ? 0.5 : 1,
              backgroundColor:"#ff6600",
              cursor: disabled ? "auto" : "pointer",
              width: 150
            }}
            onClick={(e) => sendData(e)}
          >
            <span style={{marginRight: 5}}> Send </span> 
            <ClipLoader color={'white'} loading={loading} size={15} />
          </button>
          <p 
            style={{ 
              color: formData.responseText === 'error' ? 'red' : 'green',
              textAlign: 'center',
              height: 16,
              marginTop: 10
            }}
         > {formData.responseText} </p>
        </form>
      </section>
    </section>
  );
}

export async function getStaticProps() {
  return {
    props: {
      captcha_site_key: process.env.RECAPTCHA_SITE_KEY
    }
  }
}

export default Contact;

其他注意事项

  • 剩余两个集成reCAPTCHA的页面,复用同样的逻辑即可:动态导入+动态key+挂载时重置
  • 不要在_document.js里手动引入reCAPTCHA脚本标签,动态导入组件会自动处理脚本加载,重复引入反而会导致实例冲突
  • invisible模式的reCAPTCHA每次提交表单后必须调用reset()方法重置实例,避免后续请求拿到过期token

内容的提问来源于stack exchange,提问作者Riyasaat Ahmed Rahul

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 04:06:20