• Author: Kyle Murphy
  • Posted: 24 May 2022 (updated on 20 Sept 2024)

Reactive Forms with Netlify.

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.