Zach Koch
Posted on September 4, 2024
Using useFormState and useActionState
Next.js documentation currently encourages the use of useFormState
and useActionState
to submit form data to your server actions.
I will show you how I got Google's reCaptcha v3 working with useFormState
. This may also apply to useActionState
, but I have not tested it.
Setting up reCAPTCHA
Go to the reCAPTCHA v3 admin console and create a new site if you don't have one yet
Your configuration should have
- A label for your project
- Score Based (v3) selected
-
localhost
andyour site url
both added as domains
After submitting the next page will let you copy the keys that you will need to add to your project in the next section
Setting up environment variables
In your project create a file called .env.local
if you don't have one
In this file add your keys
.env.local
bash
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=yoursitekeygoeshere
RECAPTCHA_SECRET_KEY=yoursecretkeygoeshere
Make sure you have this file added to .gitignore
adding
NEXT_PUBLIC_
to our variable allows it to be accessed in the browser
react-google-recaptcha-v3
install react-google-recaptcha-v3
bash
npm i -S react-google-recaptcha-v3
This package comes with GoogleReCaptchaProvider
, a component that runs the reCaptcha script that detects bots and provides the result to any child components.
The documentation states that it should places as high as possible in the tree, but I found that I would get errors if it was outside the bounds of a client component.
In my case I wrapped it in the same file as my form component
Contact.tsx
ts
'use client';
import {
GoogleReCaptchaProvider, useGoogleReCaptcha
} from 'react-google-recaptcha-v3';
export default function Contact() {
return (
<GoogleReCaptchaProvider
reCaptchaKey={
process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''
}>
<ContactForm />
</GoogleReCaptchaProvider>
)
}
function ContactForm() {
// my form component
}
Integrate with useFormState
Normally we would set up useFormState
by providing it with a server action that handles our form data.
In this case we want to add the reCAPTCHA data just before submitting to the server.
To do this we will
- make a function with that accepts
prevState
andformData
- get our captcha response from the reCAPTCHA script using executeRecaptcha
- attach the captcha response to our form as a form field
- send it off to our server action
Contact.tsx
just inside your form component
ts
const initialState: MessageState = { message: null, errors: {}};
const [state, formAction] = useFormState(addRecaptcha, initialState);
const { executeRecaptcha } = useGoogleReCaptcha();
async function addRecaptcha(prevState: MessageState, formData: FormData) {
let gRecaptchaToken = ''
if (executeRecaptcha) {
gRecaptchaToken = await executeRecaptcha('contactMessage');
}
formData.set('captcha', gRecaptchaToken);
return createMessage(prevState, formData)
}
The form action will go at the top of your form
Contact.tsx
<form action={formAction}>
// your form
</form>
When you submit, the token that is created by the captcha provider will be sent to your server action as just another field
Validating captcha data
In your server action you will recieve your captcha token as part of your form data.
You can send the token to Google to verify
If the verification fails you can treat it the same way you would treat validation for any failed field in your form.
Contact.tsx
async function validateCaptcha(captchaToken: string): Promise<boolean> {
const minimumCaptchaScore = 0.7;
const secretKey = process.env.RECAPTCHA_SECRET_KEY || '';
const data = new FormData();
data.append('secret', secretKey);
data.append('response', captchaToken);
const captchaResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: "POST",
body: data,
});
const res = await captchaResponse.json();
console.log(`captcha score: ${res.score}`);
return res.score && res.score >= minimumCaptchaScore;
}
const valid = await validateCaptcha(formData.get('captcha') ?? '')
The captcha score I'm still playing with, there doesn't seem to be a clear answer, but in my testing I usually get a 0.9
A friend of mine pointed out that using
FormData
would be more secure than building the url with a template string.
Hopefully that is enough to help you integrate captcha. I'll update this for useActionState
in the future. I am currently using this on my own site. Let me know in the comments if you spot any issues or if anything is unclear.
Posted on September 4, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.