devto 2026-07-29 원문 보기 ↗
Every time I add Google reCAPTCHA to a form I end up rewriting the same glue: load the script exactly once, register and clean up global callbacks, handle the token expiring, and remember that v2 and v3 work nothing alike. So I packaged it into three tiny libraries, one per framework, with the same API:
They are zero-dependency, fully typed, tested, and MIT. The part I am happiest with: you switch between v2 and v3 with a single version prop.
npm install recaptcha-react
# or: npm install recaptcha-vue
# or: npm install recaptcha-angular
The framework itself is a peer dependency, so nothing else is added to your bundle.
reCAPTCHA v2 (the "I'm not a robot" checkbox) and reCAPTCHA v3 (invisible, score-based) share a name and almost nothing else:
| v2 checkbox | v3 score-based | |
|---|---|---|
| UI | a visible widget | no widget, just a floating badge |
| Token | arrives when the user clicks | you call execute(action) in code |
| Model | wait for an event | get a promise, usually at submit time |
Most wrappers pick one. These handle both behind one component, and keep the rest of the surface identical: the same verify callback fires in both cases, so your form logic does not change when you switch.
React
import { ReactRecaptcha, useRecaptcha } from 'recaptcha-react'
function Form() {
const { isVerified, onVerify, onExpire, onError } = useRecaptcha()
return (
<form>
<ReactRecaptcha sitekey="SITE_KEY" onVerify={onVerify} onExpire={onExpire} onError={onError} />
<button disabled={!isVerified}>Submit</button>
</form>
)
}
Vue
<VueRecaptcha sitekey="SITE_KEY" @verify="onVerify" @expire="onExpire" @error="onError" />
Angular (standalone, works with ngModel and reactive forms because it implements ControlValueAccessor)
<recaptcha-v2 sitekey="SITE_KEY" (verify)="captcha.onVerify($event)"></recaptcha-v2>
v3 renders no widget, so instead of waiting for a click you ask for a token when you are about to submit:
const captcha = useRef<RecaptchaHandle>(null)
async function handleSubmit(e) {
e.preventDefault()
const token = await captcha.current!.execute('login')
await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 'g-recaptcha-response': token }),
})
}
return <ReactRecaptcha ref={captcha} sitekey="V3_SITE_KEY" version="v3" />
Same idea in Vue (await recaptchaRef.value.execute('login')) and Angular (await this.captcha.execute('login')). Because execute() also fires verify, the useRecaptcha hook / composable / service you already wired up keeps working.
expire event (v2) and a reset() you call after every submit keep the state honest. For v3 you just call execute() again at the next submit.error instead of a form that silently never works.Client state is never proof of anything. Always verify the token on your backend against https://www.google.com/recaptcha/api/siteverify with your secret key. For v3 the response also includes a score (0.0 to 1.0) and the action, so reject low scores and confirm the action matches what you expected.
Feedback and issues welcome, especially on the API shape and anything I missed for the invisible / compact flows.