Next.js Discord

Discord Forum

using ReCAPTCHA v3 with useFormState and server actions

Answered
Birman posted this in #help-forum
Open in Discord
BirmanOP
hello,
i am currently in the process of adding recaptcha to a next 14 project.
there is a contact form on the website using nodemailer to send an email.
i am trying to generate the token needed for verification when a form is submitted.
the form in question is using a server action to perform validation and error handling before an email is sent. this is where i would like to verify the token.

as i understand though, most npm packages and wrappers for recaptcha v3 all imply client side usage with inputs managed via setState. since i use formData and a server action, i haven't found yet how to generate a token on the client on submission of the form and sending it to the server action. most medium tutorials propose client side solutions like disabling a button but that kind of seems pointless to me, which is why i'm trying to do it inside the server action.

i have tried a couple of things:
- onSubmit alongside action to generate token and then submit with requestSubmit, didn't work
- wonky attempt at dynamic binding to a server action, didn't work

any help is appreciated.
code below in 5 minutes.
Answered by Birman
ended up using route handlers instead. seems like server actions need some work still
View full answer

9 Replies

BirmanOP
import { submitContact } from "@/lib/actions";

function Toasts({ data }) {...}

export default function Contact() {
    const [toasts, setToasts] = useState([]);
    const [form, submit] = useFormState(submitContact, null);
    const { pending } = useFormStatus();

    useEffect(() => {
        if (form && !toasts.includes(form) && !form.expired) {
            setToasts((prevState) => [...prevState, form]);
            setTimeout(() => {
                form.expired = true;
                setToasts((prevState) =>
                    prevState.filter((toast) => toast.id !== form.id),
                );
            }, 5000);
        }
    }, [form, toasts]);

    //TODO: generate token on submission and append to data sent to server

    return (
        <>
            <Toasts data={toasts} />
            <form className={styles.form} action={submit}>//...</form>
        </>
    );
};
take a quick look at this and see if it works for you : https://github.com/snelsi/next-recaptcha-v3
BirmanOP
i have tried this, but it implies the use of useState, which i'm not using. i use a form server action and useFormState
Ok welp, I'm not sure if there is a purely server only way to do this :/
BirmanOP
ended up using route handlers instead. seems like server actions need some work still
Answer
Polar bear
@Birman - I'm in the same situation, would you happen to have any code snippets you could share that show how you solved this one?
@Polar bear <@333224383986270208> - I'm in the same situation, would you happen to have any code snippets you could share that show how you solved this one?
BirmanOP
yeah sure. i used a route handler and useState instead though. i didn't succeed with useFormStatus and server actions.

Contact.jsx
"use client";
import { useState } from "react";
export default function Contact() {
    const [pending, setPending] = useState(false); //loading state
    const [input, setInput] = useState("");
    async function handleSubmit() {
        e.preventDefault();
        setPending(true);
        await grecaptcha.ready(() => {
            grecaptcha
                .execute("site key", {action: "relevant name", })
                .then(async (token) => {
                    try {
                        let res = await fetch("/api/contact", {
                            method: "POST",
                            body: JSON.stringify({
                                input: input,
                                token: token,
                            }),
                        });
                        res = await res.json();
                    } catch (e) {
                        console.error(e);
                    }
                    setPending(false);
                });
        });
    }
    return (
        <form onSubmit={handleSubmit}>
              <input
                  type="text"
                  name="input"
                  id="input"
                  onChange={(e) => setInput(e.target.value)}
              />
        </form>
    )
}


/api/contact/route.js
export async function POST(request) {
    const res = await request.json();
    async function verifyCaptcha(token) {
        const res = await fetch(
            `https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${token}`,
            { method: "POST", cache: "no-store" },
        );
        const json = await res.json();
        return json.score >= 0.5;
    }
    //captcha & form validation, send email, etc...
}
Polar bear
That was incredibly helpful, thanks for sharing!