如何将Google reCAPTCHA v2与react-hook-form集成并处理异步请求?
解决React Hook Form集成Google reCAPTCHA v2的两个核心问题
问题根源梳理
你遇到的两个问题本质是:
- 表单值与reCAPTCHA回调的上下文割裂,导致回调无法获取表单数据
- 错误地将API请求逻辑从
onSubmit移到reCAPTCHA回调,丢失了原有的异步等待能力
下面是两种直接可行的解决方案,优先推荐方案一(更符合React Hook Form的设计逻辑):
方案一:在异步onSubmit中手动触发reCAPTCHA验证
核心思路是保留onSubmit的异步特性,先完成表单验证,再手动触发reCAPTCHA,最后在回调中结合表单数据发起API请求。
完整代码示例
import { useForm } from 'react-hook-form'; import ReCAPTCHA from 'react-google-recaptcha'; import { useRef, useState } from 'react'; const EmailSubscriptionForm = () => { const { register, handleSubmit, formState: { errors, isValid }, getValues } = useForm({ mode: 'onSubmit' // 仅在提交时触发验证,按需调整 }); const recaptchaRef = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); // 异步onSubmit逻辑:先验证表单,再触发reCAPTCHA const onSubmit = async () => { if (!isValid) { // 手动触发表单验证,确保所有字段符合规则 const validationPassed = await handleSubmit(() => {})(); if (!validationPassed) return; } setIsSubmitting(true); const email = getValues('email'); // 通过Promise包裹reCAPTCHA验证+API请求流程,实现异步等待 return new Promise(async (resolve, reject) => { try { // 手动触发reCAPTCHA验证 recaptchaRef.current?.execute(); // 这里的resolve/reject会在reCAPTCHA回调中调用 window.recaptchaSubmitContext = { email, resolve, reject }; } catch (err) { setIsSubmitting(false); reject(err); } }); }; // reCAPTCHA回调:获取token后发起API请求 const handleRecaptchaChange = async (token) => { if (!token || !window.recaptchaSubmitContext) return; const { email, resolve, reject } = window.recaptchaSubmitContext; delete window.recaptchaSubmitContext; try { // 发起实际API请求 const response = await fetch('/api/subscribe-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, recaptchaToken: token }) }); if (!response.ok) throw new Error('订阅请求失败'); const result = await response.json(); resolve(result); // 处理成功逻辑:比如提示用户订阅成功 alert('订阅成功!'); } catch (err) { reject(err); // 处理错误逻辑:比如提示用户重试 alert(`订阅失败:${err.message}`); } finally { setIsSubmitting(false); // 重置reCAPTCHA,允许下次验证 recaptchaRef.current?.reset(); } }; return ( <form onSubmit={(e) => { e.preventDefault(); onSubmit().catch(err => console.error('提交失败:', err)); }} > <div> <input type="email" {...register('email', { required: '邮箱不能为空', pattern: { value: /^\S+@\S+\.\S+$/, message: '请输入有效的邮箱地址' } })} placeholder="输入你的邮箱" disabled={isSubmitting} /> {errors.email && <span className="error">{errors.email.message}</span>} </div> {/* 隐藏式reCAPTCHA,也可以用size="normal"显示验证按钮 */} <ReCAPTCHA ref={recaptchaRef} sitekey="YOUR_RECAPTCHA_SITE_KEY" onChange={handleRecaptchaChange} size="invisible" /> <button type="submit" disabled={isSubmitting}> {isSubmitting ? '提交中...' : '订阅'} </button> </form> ); }; export default EmailSubscriptionForm;
方案优势
- 保留异步能力:通过Promise包裹reCAPTCHA和API请求流程,
onSubmit依然可以等待整个流程完成,便于处理加载状态和结果反馈 - 表单数据可控:通过
getValues在表单验证通过后获取邮箱值,确保拿到的是合法数据 - 逻辑清晰:表单验证、验证码触发、API请求的流程完全可控,符合React Hook Form的设计范式
方案二:用状态存储表单值(简化版)
如果觉得方案一的Promise逻辑稍复杂,可以通过watch实时同步表单值到组件状态,再在reCAPTCHA回调中使用:
import { useForm } from 'react-hook-form'; import ReCAPTCHA from 'react-google-recaptcha'; import { useRef, useState, useEffect } from 'react'; const EmailForm = () => { const { register, handleSubmit, watch } = useForm(); const recaptchaRef = useRef(null); const [email, setEmail] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); // 实时监听邮箱字段变化,同步到状态 const emailWatch = watch('email'); useEffect(() => { setEmail(emailWatch); }, [emailWatch]); // 同步onSubmit:仅触发reCAPTCHA验证 const onSubmit = async (data) => { setIsSubmitting(true); recaptchaRef.current?.execute(); }; // reCAPTCHA回调:使用状态中的邮箱值发起请求 const handleRecaptchaChange = async (token) => { if (!token) return; try { const response = await fetch('/api/submit-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, recaptchaToken: token }) }); if (!response.ok) throw new Error('请求失败'); alert('提交成功!'); } catch (err) { alert(`提交失败:${err.message}`); } finally { setIsSubmitting(false); recaptchaRef.current?.reset(); } }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input type="email" {...register('email', { required: '邮箱不能为空', pattern: /^\S+@\S+\.\S+$/ })} placeholder="输入邮箱" disabled={isSubmitting} /> <ReCAPTCHA ref={recaptchaRef} sitekey="YOUR_RECAPTCHA_SITE_KEY" onChange={handleRecaptchaChange} size="invisible" /> <button type="submit" disabled={isSubmitting}> {isSubmitting ? '提交中...' : '提交'} </button> </form> ); };
方案说明
- 适合简单场景,通过
watch实时同步表单值,无需手动管理Promise - 缺点是
watch会频繁触发状态更新,相比方案一稍显冗余
内容的提问来源于stack exchange,提问作者antonwilhelm
相关产品推荐
相关产品推荐

