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.