How I Improved My Form Handling, Validation, and EmailJS in React

I’ve been working on my React project today and focused on handling forms properly. I combined React Hook Form, Zod validation, and EmailJS to create a real working contact form that actually sends emails.
If you’re a junior developer who’s trying to understand how all these pieces fit together, I hope this post helps you save a few hours of confusion

Step 1: Setting up React Hook Form + Zod
First, I installed the necessary libraries:

npm install react-hook-form zod @hookform/resolvers sonner

Then, I imported everything in my ContactForm.jsx file:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { toast } from "sonner";

I created a simple validation schema using Zod:

const contactSchema = z.object({
  name: z.string().min(1, "Name is required").max(100),
  email: z.string().email("Invalid email").max(50),
  message: z.string().min(1, "Message is required").max(1000),
});

Zod lets you describe what valid data looks like in plain English, and React Hook Form handles the rest automatically.

Step 2: Building the form

export default function ContactForm() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(contactSchema),
  });

  const onSubmit = async (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-md mx-auto">
      <input
        type="text"
        {...register("name")}
        placeholder="Your name"
        className="border rounded p-2 w-full"
      />
      {errors.name && <p className="text-red-500">{errors.name.message}</p>}

      <input
        type="email"
        {...register("email")}
        placeholder="Your email"
        className="border rounded p-2 w-full"
      />
      {errors.email && <p className="text-red-500">{errors.email.message}</p>}

      <textarea
        {...register("message")}
        placeholder="Your message"
        className="border rounded p-2 w-full"
      />
      {errors.message && <p className="text-red-500">{errors.message.message}</p>}

      <button
        type="submit"
        disabled={isSubmitting}
        className="bg-blue-500 text-white px-4 py-2 rounded"
      >
        {isSubmitting ? "Sending..." : "Send"}
      </button>
    </form>
  );
}
export default function ContactForm() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(contactSchema),
  });

  const onSubmit = async (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-md mx-auto">
      <input
        type="text"
        {...register("name")}
        placeholder="Your name"
        className="border rounded p-2 w-full"
      />
      {errors.name && <p className="text-red-500">{errors.name.message}</p>}

      <input
        type="email"
        {...register("email")}
        placeholder="Your email"
        className="border rounded p-2 w-full"
      />
      {errors.email && <p className="text-red-500">{errors.email.message}</p>}

      <textarea
        {...register("message")}
        placeholder="Your message"
        className="border rounded p-2 w-full"
      />
      {errors.message && <p className="text-red-500">{errors.message.message}</p>}

      <button
        type="submit"
        disabled={isSubmitting}
        className="bg-blue-500 text-white px-4 py-2 rounded"
      >
        {isSubmitting ? "Sending..." : "Send"}
      </button>
    </form>
  );
}

At this point, validation was already working. If I left any field empty or entered an invalid email, friendly red error messages appeared.

Step 3: Sending real emails with EmailJS

I created a free account on EmailJS, added my email template, and grabbed the following values from my dashboard:

Service ID
Template ID
Public Key

These are all safe to use in the frontend.

Then I installed the library:

npm install @emailjs/browser

And updated my onSubmit function:
import emailjs from “@emailjs/browser”;

const onSubmit = async (data) => {
  try {
    const templateParams = {
      from_name: data.name,
      from_email: data.email,
      message: data.message,
    };

    await emailjs.send(
      "your_service_id",
      "your_template_id",
      templateParams,
      "your_public_key"
    );

    toast.success("Message sent successfully!");
    reset();
  } catch (error) {
    console.error("Error sending message:", error);
    toast.error("Failed to send message. Please try again later.");
  }
};

When I hit submit, I saw a toast saying “Message sent successfully!” and then I got the actual email in my inbox 🎉
That was a big confidence boost.

What I Learned Today

data only exists inside onSubmit. React Hook Form passes the form data there after validation.
Zod plus React Hook Form makes validation clean and readable.
EmailJS works safely from the frontend when using the public key.
Async/await plus try/catch make form submissions easy to handle.
Adding a loading state and toasts improves UX.

Final Thoughts

This was a big step toward writing more professional React code. Forms are no longer intimidating. Now I understand the flow:

User types → Validation → onSubmit → EmailJS → Success toast → Reset form.

That’s a simple but powerful pattern that I’ll reuse again and again.
If you’re just starting with React forms, play around, log your data, and celebrate small wins. They add up fast.

✍️ I’m a self-taught front-end developer learning React and sharing my journey. If you’ve gone through something similar, I’d love to hear about it in the comments!

Similar Posts