Graphic by Custom Code IT focusing on reactive forms with Netlify. The image features a dark background with the Netlify logo prominently displayed in the centre, represented by teal text and a stylised spark icon. The text at the bottom reads ‘Reactive Forms with Netlify’ in bold white letters. The Custom Code IT logo is shown at the top, represented by curly braces and a gear icon. The graphic highlights the integration of reactive forms with Netlify’s platform for dynamic web development.

Reactive Forms with Netlify.

A guide on how to get Netlify forms working with NextJS, Typescript, Yup, and React Hook Form.

Introduction

Building a website with a contact page and accompanying form is quite common. However, posting forms to Netlify with Next.js can be a little tricky. Let's cover some of the issues I ran into and how I solved them.

I will likely gloss over some of the finer points of React Hook Form, validation etc. but will create future posts to cover these topics in more detail.

Follow along with the repository here

 

Setup NextJS

    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

We also need to install the following dev dependencies as we are using TypeScript:

    npm install --sav-dev @types/yup

Let's breakdown these packages:

  • @hookform/devtools - Used to display dev tools along side your form. Useful for debugging
  • @hookform/error-message - Allows us to easily show error messages
  • @hookform/resolvers - Gives us the ability to use common form validation libraries (e.g. Yup)
  • react-hook-form - The main React Hook Form package itself.
  • react-select - Makes select boxes easy to work with.
  • yup - Used for form validation
  • sass - Using SASS for styling 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. Due to 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.

NOTE: It is recommended you build on this example if you plan on using it. Consider features like a loading state while the form is submitted, better user feedback when complete, and perhaps navigating to a thankyou/success page upon completion.

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

A few things to note here. We are setting data-netlify and data-netlify-honeypot attributes to tell Netlify that we want to use this form for submissions. The honeypot field also greatly reduces the amount of spam you're likely to get.

More information on integrating forms in Netlify can be found here

NOTE: 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 are also doing some validation on this form using Yup. Here's 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,
}

 

NextJS Config

I have also configured the Locale on the site

next.config.js

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

module.exports = nextConfig

 

Conclusion

Hopefully you have gained some value from this information. Feedback is welcome; 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 create an issue on the GitHub repository.