Reactive Forms with Netlify

  • Author: Kyle Murphy
  • Posted: 24 May 2022 (updated on 7 Oct 2026)
The Netlify logo above the words Reactive Forms with Netlify, with the Custom Code IT logo at the top.

Building a website with a contact page and form is common, but posting forms to Netlify from Next.js can be tricky. This post covers the issues I ran into and how I solved them.

I gloss over some of the finer points of React Hook Form and validation here. You can follow along with the repository.

Set up Next.js

npx create-next-app@latest --typescript --use-npm

Install packages

We need to install the following packages:

npm install @hookform/devtools @hookform/error-message @hookform/resolvers react-hook-form react-select yup sass

Yup ships its own TypeScript types, so there are no extra type packages to install. Here is what each package does:

  • @hookform/devtools: displays dev tools alongside your form. Useful for debugging
  • @hookform/error-message: lets us show error messages easily
  • @hookform/resolvers: lets us use common validation libraries, such as Yup
  • react-hook-form: the main React Hook Form package
  • react-select: makes select boxes easy to work with
  • yup: used for form validation
  • sass: used to style the form

The form component

I have set up a component called the-form. It's broken up into the following parts.

Select options

These are the select options we want to make available in the form:

components/the-form/the-form.tsx

const options:StringValueSelect[] = [
    { value: 'red', label: 'Red' },
    { value: 'green', label: 'Green' },
    { value: 'blue', label: 'Blue' },
    { value: 'yellow', label: 'Yellow' },
    { value: 'orange', label: 'Orange' },
];

React Hook Form setup

This block configures React Hook Form, how it should validate and the default form values.

components/the-form/the-form.tsx

const { control, register, reset, handleSubmit, formState: { errors } } = useForm<TheForm>({
    mode: 'all',
    reValidateMode: 'onChange',
    resolver: yupResolver(theFormValidator),
    criteriaMode: "firstError",
    shouldFocusError: true,
    shouldUnregister: true,
    defaultValues: {
        favorite_colour_select: null,
    }
});

Submit function

This function handles submitting the form. Because of the way Netlify handles form requests, we need to set up the form structure first, encode the form data, and then submit it. I am using Fetch here, but Axios also works.

Build on this example if you plan to use it. Add a loading state while the form is submitted, better feedback when it completes, and a thank-you page.

components/the-form/the-form.tsx

function onSubmit(form:TheForm)
{
    const formDetails = {
        "form-name": form.form_name,
        "bot-field": form.bot_field,
        "first-name": form.first_name,
        "last-name": form.last_name,
        "email": form.email,
        "favorite-colour": form.favorite_colour_select?.value ?? '',
    }

    const data = new URLSearchParams(formDetails).toString();

    fetch("/", {
        method: "POST",
        body: data,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        }
    })
        .then(() => {
            reset();
            alert("FORM SUBMITTED!")
        })
        .catch((error) => {
            alert("ERROR SUBMITTING FORM! CHECK CONSOLE");
            console.error(error)
        });
    console.log(form);
}

The component

We set the data-netlify and data-netlify-honeypot attributes to tell Netlify to collect submissions from this form. The honeypot field also cuts down on spam.

Netlify has more on integrating forms in a React app.

The form_name and bot_field inputs are required to make this work with Next.js.

components/the-form/the-form.tsx

<form name="contact" onSubmit={handleSubmit(onSubmit)} data-netlify={true} data-netlify-honeypot={"bot_field"} className={styles.form}>

    <input type="hidden" value="contact" {...register("form_name")} />

    <div className={styles.formGroupHidden}>
        <label htmlFor="bot_field" >Are you a bot?</label>
        <input {...register("bot_field")} />
    </div>

    <div className={styles.formGroup}>
        <label htmlFor="first_name" >First Name</label>
        <input {...register("first_name")}/>
        <span className={styles.formError}><ErrorMessage name={"first_name"} errors={errors} /></span>
    </div>

    <div className={styles.formGroup}>
        <label htmlFor="last_name" >Last Name</label>
        <input {...register("last_name")}/>
        <span className={styles.formError}><ErrorMessage name={"last_name"} errors={errors} /></span>
    </div>

    <div className={styles.formGroup}>
        <label htmlFor="email" >Email</label>
        <input type="email" {...register("email")}/>
        <span className={styles.formError}><ErrorMessage name={"email"} errors={errors} /></span>
    </div>

    <div className={styles.formGroup}>
        <label htmlFor="favorite_colour_select" >Favorite Colour</label>
        <Controller name={'favorite_colour_select'}
            control={control}
            render={({field: {value, onChange}}) => <Select
                id={"favorite_colour_select"}
                instanceId={"favorite_colour_select"}
                placeholder={"Pick your favorite colour..."}
                isClearable={true}
                options={options}
                value={value}
                onChange={onChange}
            />}
        />
        <span className={styles.formError}><ErrorMessage name={"favorite_colour_select"} errors={errors} /></span>
    </div>

    <div className={styles.formControls}>
        <input type={"submit"} value={"Submit"}/>
    </div>
</form>

Validation

We also validate this form using Yup. Here is what the schema looks like:

validation/the-form.ts

import * as Yup from "yup";

export const theFormValidator = Yup.object().shape({

    first_name: Yup.string()
        .required("First name is required"),

    last_name: Yup.string()
        .required("Last name is required."),

    email: Yup.string()
        .nullable(true)
        .email('Invalid email.')
        .transform((v, o) => o === '' ? null : v)
        .required("Email is required."),

    favorite_colour_select: Yup.object().shape({
        label: Yup.string(),
        value: Yup.string(),
    })
        .nullable(true)
        .required("Favorite colour is required."),
});

Types

I have also typed the form component and created a separate type for the select box.

types/the-form.ts

import {StringValueSelect} from "./select";

export type TheForm = {
    form_name: string;
    bot_field: string;
    first_name: string;
    last_name: string;
    email: string;
    favorite_colour: string;
    favorite_colour_select: StringValueSelect | null;
}

types/select.ts

export type NumberValueSelect = {
    value: number,
    label: string,
};

export type StringValueSelect = {
    value: string,
    label: string,
}

Next.js config

I also set the locale for the site:

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  i18n: {
    locales: ["en"],
    defaultLocale: "en",
  },
}

module.exports = nextConfig

Feedback

If you have something to add, a better way to do it, or something I missed, you can reach me on the contact page or open an issue on the GitHub repository.

Kyle Murphy

Understanding Domain Names & Websites

What a domain name is, why we use them, how to register one and how a domain name connects to your website, in plain English.

  • websites
  • hosting
  • technical
  • domains

Kyle Murphy

Must-Have Content On Your Website

Content ideas for a small business website: testimonials, image galleries, project portfolios, staff profiles, client interviews and a blog.

  • websites

You made it this far, you should probably contact us.

Take a moment to celebrate, give that scrolling finger a break, then contact us because we'd love to hear from you.

0466 624 345

Tell us how we can help you.