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!