Kyle Murphy
Static vs Dynamic Websites: Choosing the Right Tool for the Job
Static or dynamic website? A plain-English look at the pros and cons of each, from hosting cost and security to editing, plugins and maintenance.
- websites
- hosting

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.
npx create-next-app@latest --typescript --use-npm
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 Yupreact-hook-form: the main React Hook Form packagereact-select: makes select boxes easy to work withyup: used for form validationsass: used to style the formI have set up a component called the-form. It's broken up into the following parts.
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' },
];
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,
}
});
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);
}
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>
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."),
});
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,
}
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
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
Static or dynamic website? A plain-English look at the pros and cons of each, from hosting cost and security to editing, plugins and maintenance.
Kyle Murphy
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.
Kyle Murphy
Content ideas for a small business website: testimonials, image galleries, project portfolios, staff profiles, client interviews and a blog.
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