3 Ways to Handle Form Submission in NextJS

Cover Image for 3 Ways to Handle Form Submission in NextJS
toofancoder
toofancoder

If you want to head over to the code on Github, Form Management with NextJS.

We all have been using forms in our web applications. Whether it's a simple contact form or a complex form with multiple fields, form handling is an essential part of any web application. In this post, we'll explore 3 different approaches to handle form submissions in NextJS: using Formik and Yup, using React Hook Form, and handling forms without any library.

To keep this post short, we'll use a simple contact form with name, email, password and phone fields. We will also additionally include a checkbox for accepting terms and conditions.

We are using the following versions for this example:

  • NextJS - 14.2.15
  • ReactJS - 18.0.0
  • Formik - 2.2.9
  • Yup - 0.3.5
  • TailwindCSS - 3.4.1

1. Using Vanilla Form Handling

Vanilla form handling is the simplest way to handle form submissions in NextJS. It involves using the native HTML form elements and handling the form data in the component's state. For this, we will not be using any library.

The form UI in all the examples will be the same as shown below:

Form UI

Below is the code for the form submission without using any library:

WithoutUsingLibrary.jsx.

import { useState } from "react";
import FormHeader from "./FormHeader";
import Modal from "./Modal";

export default function WithoutUsingLibrary() {
  // This is where we will hold the form data
  const [formData, setFormData] = useState({
    fullName: "",
    email: "",
    password: "",
    phone: "",
    terms: false,
  });
  const [errors, setErrors] = useState({});
  const [showModal, setShowModal] = useState(false);
  const [submittedData, setSubmittedData] = useState(null);

  const validateForm = () => {
    const newErrors = {};

    if (!formData.fullName.trim()) {
      newErrors.fullName = "Full name is required";
    }

    if (!formData.email.trim()) {
      newErrors.email = "Email is required";
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = "Email is invalid";
    }

    if (!formData.password) {
      newErrors.password = "Password is required";
    } else if (formData.password.length < 6) {
      newErrors.password = "Password must be at least 6 characters";
    }

    if (!formData.terms) {
      newErrors.terms = "You must accept the terms and conditions";
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData((prev) => ({
      ...prev,
      [name]: type === "checkbox" ? checked : value,
    }));
    // Clear error when user starts typing
    if (errors[name]) {
      setErrors((prev) => ({
        ...prev,
        [name]: undefined,
      }));
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (validateForm()) {
      try {
        // Store the submitted data
        setSubmittedData(formData);
        setShowModal(true);

        // Reset form after successful submission
        setFormData({
          fullName: "",
          email: "",
          password: "",
          phone: "",
          terms: false,
        });
      } catch (error) {
        console.error("Submission error:", error);
      }
    }
  };

  return (
    <div className="py-8 w-full max-w-md flex items-start justify-center">
      <div>
        <FormHeader title={"Not Using Any Library"} />

        <form className="space-y-4" onSubmit={handleSubmit}>
          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="fullName">
              Full Name
            </label>
            <input
              type="text"
              id="fullName"
              name="fullName"
              value={formData.fullName}
              onChange={handleChange}
              className={`w-full px-3 py-2 border ${
                errors.fullName
                  ? "border-red-500"
                  : "border-gray-300 dark:border-gray-700"
              } rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800`}
            />
            {errors.fullName && (
              <p className="text-red-500 text-xs mt-1">{errors.fullName}</p>
            )}
          </div>

          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="email">
              Email
            </label>
            <input
              type="email"
              id="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
              className={`w-full px-3 py-2 border ${
                errors.email
                  ? "border-red-500"
                  : "border-gray-300 dark:border-gray-700"
              } rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800`}
            />
            {errors.email && (
              <p className="text-red-500 text-xs mt-1">{errors.email}</p>
            )}
          </div>

          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="password">
              Password
            </label>
            <input
              type="password"
              id="password"
              name="password"
              value={formData.password}
              onChange={handleChange}
              className={`w-full px-3 py-2 border ${
                errors.password
                  ? "border-red-500"
                  : "border-gray-300 dark:border-gray-700"
              } rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800`}
            />
            {errors.password && (
              <p className="text-red-500 text-xs mt-1">{errors.password}</p>
            )}
          </div>

          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="phone">
              Phone Number
            </label>
            <input
              type="tel"
              id="phone"
              name="phone"
              value={formData.phone}
              onChange={handleChange}
              className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
            />
          </div>

          <div className="flex items-center gap-2">
            <input
              type="checkbox"
              id="terms"
              name="terms"
              checked={formData.terms}
              onChange={handleChange}
              className={`rounded border-gray-300 dark:border-gray-700 text-blue-500 focus:ring-blue-500 ${
                errors.terms ? "border-red-500" : ""
              }`}
            />
            <label
              className="text-sm text-gray-600 dark:text-gray-400"
              htmlFor="terms"
            >
              I agree to the Terms and Conditions
            </label>
          </div>
          {errors.terms && (
            <p className="text-red-500 text-xs">{errors.terms}</p>
          )}

          <button
            type="submit"
            className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
          >
            Submit Form
          </button>
        </form>
      </div>

      {showModal && submittedData && <Modal data={submittedData} />}
    </div>
  );
}

2. Using Formik and Yup

Formik is a popular library for handling forms in React. It provides a set of tools for managing form state, validation, and submission. Yup is a schema builder for runtime value parsing. It allows you to define validation rules for your form fields and provides a way to validate the form data before submission. For this method of form submission, we will need to install the formik and yup libraries.

Below is the code for the form submission using Formik and Yup:

UsingFormikYupLibrary.jsx.

import { ErrorMessage, Field, Form, Formik } from "formik";
import { useState } from "react";
import * as Yup from "yup";
import FormHeader from "./FormHeader";
import Modal from "./Modal";

// Validation schema using Yup
const validationSchema = Yup.object().shape({
  fullName: Yup.string().required("Full name is required"),
  email: Yup.string().email("Email is invalid").required("Email is required"),
  password: Yup.string()
    .min(6, "Password must be at least 6 characters")
    .required("Password is required"),
  phone: Yup.string(),
  tos: Yup.boolean().oneOf([true], "You must accept the terms and conditions"),
});

export default function UsingFormikYupLibrary() {
  const [showModal, setShowModal] = useState(false);
  const [submittedData, setSubmittedData] = useState(null);

  return (
    <div className="p-8 w-full max-w-md flex items-start justify-center">
      <div>
        <FormHeader title={"Using Formik & Yup"} />

        <Formik
          className="space-y-6"
          initialValues={{
            fullName: "",
            email: "",
            password: "",
            phone: "",
            tos: false,
          }}
          validationSchema={validationSchema}
          onSubmit={(values) => {
            setSubmittedData(values);
            setShowModal(true);
          }}
        >
          {({ handleChange, handleBlur }) => (
            <Form className="space-y-4">
              <div className="space-y-2">
                <label className="block text-sm font-medium" htmlFor="fullName">
                  Full Name
                </label>
                <Field
                  type="text"
                  id="fullName"
                  name="fullName"
                  className="w-full px-3 py-2 border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
                />
                <ErrorMessage
                  name="fullName"
                  component="p"
                  className="text-red-500 text-xs mt-1"
                />
              </div>

              <div className="space-y-2">
                <label className="block text-sm font-medium" htmlFor="email">
                  Email
                </label>
                <Field
                  type="email"
                  id="email"
                  name="email"
                  className="w-full px-3 py-2 border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
                />
                <ErrorMessage
                  name="email"
                  component="p"
                  className="text-red-500 text-xs mt-1"
                />
              </div>

              <div className="space-y-2">
                <label className="block text-sm font-medium" htmlFor="password">
                  Password
                </label>
                <Field
                  type="password"
                  id="password"
                  name="password"
                  className="w-full px-3 py-2 border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
                />
                <ErrorMessage
                  name="password"
                  component="p"
                  className="text-red-500 text-xs mt-1"
                />
              </div>

              <div className="space-y-2">
                <label className="block text-sm font-medium" htmlFor="phone">
                  Phone Number
                </label>
                <Field
                  type="tel"
                  id="phone"
                  name="phone"
                  className="w-full px-3 py-2 border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
                />
              </div>

              <div className="flex items-center gap-2">
                <Field
                  type="checkbox"
                  id="tos"
                  name="tos"
                  className="rounded border-gray-300 text-blue-500 focus:ring-blue-500"
                />
                <label
                  className="text-sm text-gray-600 dark:text-gray-400"
                  htmlFor="tos"
                >
                  I agree to the Terms and Conditions
                </label>
              </div>
              <ErrorMessage
                name="tos"
                component="p"
                className="text-red-500 text-xs"
              />

              <button
                type="submit"
                className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
              >
                Submit Form
              </button>
            </Form>
          )}
        </Formik>
      </div>

      {showModal && submittedData && <Modal data={submittedData} />}
    </div>
  );
}

3. Using React Hook Form

The third method for submission of a form is using React Hook Form. For this you are going to need to install the react-hook-form library.

UsingReactFormsLibrary.jsx.

"use client";

import { useCallback, useState } from "react";
import { useForm } from "react-hook-form";
import FormHeader from "./FormHeader";
import Modal from "./Modal";

export default function UsingReactFormsLibrary() {
  const {
    register,
    handleSubmit,
    getValues,
    formState: { errors },
  } = useForm();

  const [showModal, setShowModal] = useState(false);
  const [submittedData, setSubmittedData] = useState(null);

  const onSubmit = useCallback(() => {
    console.log(getValues());
    setSubmittedData(getValues());
    setShowModal(true);
  }, [getValues]);

  return (
    <div className="p-8 w-full max-w-md flex items-start justify-center">
      <div>
        <FormHeader title={"Using React Forms"} />
        <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="fullName">
              Fullname
            </label>
            <input
              type="text"
              id="fullName"
              name="fullName"
              {...register("fullName", {
                required: "Fullname field is required",
              })}
              className={`w-full px-3 py-2 border ${
                errors.fullName
                  ? "border-red-500"
                  : "border-gray-300 dark:border-gray-700"
              } rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800`}
            />
            {errors.fullName && (
              <p className="text-red-500 text-xs mt-1">
                {errors.fullName?.message}
              </p>
            )}
          </div>

          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="email">
              Email
            </label>
            <input
              type="email"
              id="email"
              name="email"
              {...register("email", { required: "Email is required" })}
              className={`w-full px-3 py-2 border ${
                errors.email
                  ? "border-red-500"
                  : "border-gray-300 dark:border-gray-700"
              } rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800`}
            />
            {errors.email && (
              <p className="text-red-500 text-xs mt-1">
                {errors.email?.message}
              </p>
            )}
          </div>

          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="password">
              Password
            </label>
            <input
              type="password"
              id="password"
              name="password"
              {...register("password", { required: "Password is required" })}
              className={`w-full px-3 py-2 border ${
                errors.password
                  ? "border-red-500"
                  : "border-gray-300 dark:border-gray-700"
              } rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800`}
            />
            {errors.password && (
              <p className="text-red-500 text-xs mt-1">
                {errors.password?.message}
              </p>
            )}
          </div>

          <div className="space-y-2">
            <label className="block text-sm font-medium" htmlFor="phone">
              Phone Number
            </label>
            <input
              type="tel"
              id="phone"
              name="phone"
              {...register("phone", { required: "this field is required" })}
              className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
            />
          </div>

          <div className="flex items-center gap-2">
            <input
              type="checkbox"
              {...register("termsReactForms", {
                required: "You must accept the terms and conditions",
              })}
              className={`rounded border-gray-300 dark:border-gray-700 text-blue-500 focus:ring-blue-500 ${
                errors.termsReactForms ? "border-red-500" : ""
              }`}
            />
            <label
              htmlFor="termsReactForms"
              className="text-sm text-gray-600 dark:text-gray-400"
            >
              I agree to the Terms and Conditions
            </label>
          </div>
          {errors.termsReactForms && (
            <p className="text-red-500 text-xs">
              {errors.termsReactForms?.message}
            </p>
          )}

          <button
            type="submit"
            className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
          >
            Submit Form
          </button>
        </form>
      </div>

      {showModal && submittedData && <Modal data={submittedData} />}
    </div>
  );
}

When the form is submitted, we are showing the captured data in a modal for brevity. If you want a quick look at the code, you can head over to the Modal component.