Whenever we have a contact form in our application it's a good idea to implement Google Recaptcha to prevent spam messages from coming through.
Without further ado let's jump straight into it!
Project setup
Let's create a clean Remix JS project> For the ease of setup I am going to use an unstable Vite plugin (it might not be when you are reading this post)
npx create-remix@latest --template remix-run/remix/templates/unstable-vite
Form Setup
For simplicity, we will set our form in app/routes/index.tsx
import type { MetaFunction } from "@remix-run/node";
import { Form } from "@remix-run/react";
export const meta: MetaFunction = () => {
return [{ title: "Remix JS + Recaptcha v3" }, { name: "description", content: "Welcome to Remix!" }];
};
export default function Index() {
return (
<div className="page_wrapper">
<Form method="post" className="form">
<div className="form_field">
<label htmlFor="name">Name</label>
<input type="text" name="name" id="name" />
</div>
<button type="submit">Submit</button>
</Form>
</div>
);
}
As you can see there isn't much, just a single input for the name and submit button, let's add some base styles to make it look somewhat decent :)
Create app/styles.css
file
/* app/styles.css */
* {
margin: 0;
padding: 0;
}
.page_wrapper {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
.form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
}
.form_field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form_field label {
font-size: 1.2rem;
}
.form_field input {
font-size: 1rem;
padding: 0.5rem;
}
and import it in _index.tsx
import "~/styles.css";
We should be presented with something like this:
Pretty right? Not really :P but functionality is the key here.
Implementing Recaptcha
First thing let's register for Recaptcha to obtain the Site Key and Secret Key, go here and register domains that will use it(in our case it's just localhost) and save your keys.
Now create a file .env
in the root of your project and paste in both of your keys, we will need them.
//.env
RECAPTCHA_SITE_KEY = '[your_site_key]'
RECAPTCHA_SECRET_KEY = '[your_secret_key]'
react-google-recaptcha-v3 setup
We will use react-google-recaptcha-v3 and wrap our entire app with GoogleReCaptchaProvider
.
Take a look at our updated root.tsx
file
import { LoaderFunction } from "@remix-run/node";
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, json, useLoaderData } from "@remix-run/react";
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
export const loader: LoaderFunction = async () => {
return json({
ENV: {
RECAPTCHA_SITE_KEY: process.env.RECAPTCHA_SITE_KEY,
},
});
};
export default function App() {
const { ENV } = useLoaderData<typeof loader>();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<GoogleReCaptchaProvider
reCaptchaKey={ENV.RECAPTCHA_SITE_KEY}
scriptProps={{
async: false,
defer: true,
appendTo: "head",
nonce: undefined,
}}
>
<Outlet />
</GoogleReCaptchaProvider>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
As you can see we need to import GoogleReCaptchaProvider
and wrap <Outlet />
component in it. Because we have to pass our SITE_KEY
into it, we also need to use loader
function and return our SITE_KEY
from it.
getRecaptchaScore function
Let's create a new file: app/utils/getRecaptchaScore.tsx
export async function getRecaptchaScore(token: string, key: string): Promise<boolean> {
let res;
const captchData = new URLSearchParams({
secret: key,
response: token,
});
try {
// Sending a POST request to the reCAPTCHA API using fetch
res = await fetch("https://www.google.com/recaptcha/api/siteverify", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: captchData,
});
// Parsing the JSON response
res = await res.json();
} catch (e) {
// Handling errors if the reCAPTCHA verification fails
console.log("recaptcha error:", e);
}
console.log(res.score); // let's console log the score
// Checking the result of the reCAPTCHA verification
if (res && res.success && res.score > 0.5) {
// If verification is successful, continue with form submission
return true;
} else {
// If verification fails, return an error message
return false;
}
}
We will use this function to reCaptcha validate our contact form submission, it takes in token and key as its attributes and makes a request to google recaptcha API which will return us success
and score
which we will then use to return either true
if we think we should pass this form submission or false
if we think it's a robot submitting our form!
Adding reCaptcha validation to our form
Let's go back to our _index.tsx
file
First, we will use useGoogleReCaptcha
hook provided by react-google-recaptcha-v3 library in order to get our token(which we need to pass to our getRecaptchaScore function), then using useCallback
and useEffect
hook save our token in the captchaToken state. More on this in react-google-recaptcha-v3 documentation.
//_index.tsx
export default function Index() {
// Token state
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
// custom hook from reCaptcha library
const { executeRecaptcha } = useGoogleReCaptcha();
const handleReCaptchaVerify = useCallback(async () => {
if (!executeRecaptcha) {
return;
}
const token = await executeRecaptcha("yourAction");
setCaptchaToken(token);
}, [executeRecaptcha]);
// useEffect that will execute out token setting callback function
useEffect(() => {
handleReCaptchaVerify();
}, [handleReCaptchaVerify]);
return (
...
)
}
Now we need to create an action function and a way to pass our token to it. We will do it by adding a hidden input field to our Form. In our new action function, we can get our form data which has our token in it, and pass it into our getRecaptchaResult
util function.
Based on its result we will either pass and redirect to the thank you page or prevent form submission and return a message which we can then display below our form.
// _index.tsx
...
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
// form validation logic here
const token = formData.get("_captcha") as string;
const key = process.env.RECAPTCHA_SECRET_KEY as string;
// validate captcha util function
const recaptchaResult = await getRecaptchaScore(token, key);
console.log(recaptchaResult); // result of our recaptcha validation
if (!recaptchaResult) {
// your contact form submission code here
return redirect("/thank-you");
}
return json({ message: "You are a robot!" });
};
export default function Index() {
// get data from action hook
const actionData = useActionData<typeof action>();
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const { executeRecaptcha } = useGoogleReCaptcha();
const handleReCaptchaVerify = useCallback(async () => {
if (!executeRecaptcha) {
return;
}
const token = await executeRecaptcha("yourAction");
setCaptchaToken(token);
}, [executeRecaptcha]);
useEffect(() => {
handleReCaptchaVerify();
}, [handleReCaptchaVerify]);
return (
<div className="page_wrapper">
<Form method="post" className="form">
{captchaToken ? <input type="hidden" name="_captcha" value={captchaToken}></input> : null}
<div className="form_field">
<label htmlFor="name">Name</label>
<input type="text" name="name" id="name" />
</div>
<button type="submit" onSubmit={() => handleReCaptchaVerify}>
Submit
</button>
{actionData?.message ? <p>{actionData.message}</p> : null}
</Form>
</div>
);
}
And that's it! Working recaptcha validation in your Remix Contact Form.
Of course, other things would need to be implemented here like form validation and actual contact form submission logic but those parts are specific to the type of form or your backend that will handle the form submission itself.
Check on GitHub
Example repo here.
Hope it was helpful and see you at the next one! :)