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
相关产品推荐
相关产品推荐

