DEV Community

Cover image for How to create a contact form with EmailJS, Zod, ShadCN UI, TypeScript using React and NextJS.
Brendan
Brendan

Posted on

How to create a contact form with EmailJS, Zod, ShadCN UI, TypeScript using React and NextJS.

With any luck, there are enough frontend buzzwords in that title to make this an interesting post!

In this article, I will take you through the steps involved in creating a basic contact form that has been jazzed up with a few libraries that take away some of the pain during development, (and add some of their own challenges).

Breakdown

As the title suggests, we'll be using the following tools:


Assumptions

This article assumes you have already configured your application to use NextJS - with Typescript, TailwindCSS, Zod. If not, the links above all have docs on how to install and configure these tools

Additionally, you'll need an account with EmailJS. The fun part is that EmailJS offers a free teir version for their services so there's no reason not to!


Let's Get Started

Using your favourite package manager, let's get the following packages installed. In my case, I am using npm for simplicity.

npm install @emailjs/browser react-hook-form zod @hookform/resolvers
Enter fullscreen mode Exit fullscreen mode

If you haven't already configured ShadCN UI, check out their docs here for NextJS applications. Get started with ShadCN.


Create the Contact Form component

From here it's more straight-forward as we'll use a lot of ShadCN and EmailJS out of the box code, with some minor tweaks to let them work together.

Install your ShadCN components

npx shadcn-ui@latest add form button input toast textarea
Enter fullscreen mode Exit fullscreen mode

Create a Contact.tsx file in your application

Organise your imports

  • Gotcha, check the filepath for the ShadCN components matches your configuration.
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import React, { useRef } from "react";
import { z } from "zod";
import emailjs from "@emailjs/browser";
import { Button } from "~/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "~/components/ui/form";
import { Input } from "~/components/ui/input";
import { useToast } from "~/components/ui/use-toast"; //optional
import { Textarea } from "~/components/ui/textarea";
Enter fullscreen mode Exit fullscreen mode

Create the form schema with Zod

In most contact forms you'll want the persons name, email address, and their message.

//...imports
const FormSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
  email: z.string().email("Invalid email address.").min(2, {
    message: "Email must be at least 2 characters.",
  }),
  message: z.string().min(10, {
    message: "Message should be at least 10 characters.",
  }),
});
Enter fullscreen mode Exit fullscreen mode

Create a reusable component using export

On your site, you want to make it as easy as possible for people to get in contact with you, by exporting the component you can import it in various places throughout your website.

Gotcha: Make sure you pass in your own valid EmailJS Template ID, Service ID, and Public Key. Here I reference them via my .env file.

In this component, we will need to define the following:

  1. const formRef
  2. const form
  3. const onSubmit
export const Contact = () => {

  // EmailJS needs the `ref` parameter in a form, ShadCN doesn't use 
  // this by default so we have to import it.
  const formRef = useRef<HTMLFormElement | null>(null);

  // configure Zod default values for the form
  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: zodResolver(FormSchema),
    defaultValues: {
      username: "",
      email: "",
      message: "",
    },
  });
}

// Create the handler that connects to EmailJS.  
  const onSubmit = (data: z.infer<typeof FormSchema>) => {
    if (formRef.current) {
      emailjs
        .sendForm(
          env.NEXT_PUBLIC_EMAILJS_SERVICE_ID,
          env.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID,
          formRef.current,
          {
            publicKey: env.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY,
          },
        )
        .then(
          () => {
            form.reset(); //clear the fields after submission
          },
          (error) => {
            console.warn("FAILED...", JSON.stringify(error));
          },
        );
    }
  };
// ...the ShadCN form will go here.
} //end of Contact component
Enter fullscreen mode Exit fullscreen mode

Create your ShadCN form

Now that all of your functions and variables are declared, you want to create the actual React component that renders onto the screen. For this, we'll use ShadCN's form that is react-hook-form under the hood.

With this, you'll now have a well formed, semantically correct Form provided by ShadCN, with client side validation, responsive design, and a host of other benefits.

//... functions and variables
  return (
    <>
      <Form {...form}>
        <form
          ref={formRef} //Required by EmailJS
          onSubmit={form.handleSubmit(onSubmit)}
          className="w-2/3 space-y-6">
          <FormField
            name="username"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-lg">Name</FormLabel>
                <FormControl>
                  <Input
                    className="border-primary bg-white"
                    placeholder="Your Name"
                    {...field}
                  />
                </FormControl>
                <FormMessage className="text-xs text-red-600" />
              </FormItem>
            )}/>
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-lg">Email</FormLabel>
                <FormControl>
                  <Input
                    className="border-primary bg-white"
                    placeholder="Email Address"
                    {...field}
                  />
                </FormControl>
                <FormMessage className="text-xs text-red-600" />
              </FormItem>
            )}/>
          <FormField
            control={form.control}
            name="message"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-lg">Message</FormLabel>
                <FormControl>
                  <Textarea
                    className="border-primary bg-white"
                    placeholder="Type your message here."
                    id="message"
                    {...field}
                  />
                </FormControl>
                <FormMessage className="text-xs text-red-600" />
              </FormItem>
            )}/>
          <Button
            type="submit"
            className="text-md text-white hover:bg-secondary">
            {/*<PaperPlane />*/}
            Send{" "}
          </Button>
        </form>
      </Form>
    </>
  );

Enter fullscreen mode Exit fullscreen mode

Optional Bonus: Add a Toast notification

What nicer way to inform your visitors that your email has been sent, or has failed to send, than by adding a neat Toast notification to your onSubmit handler function.

For this, we'll be using the optional imports above inside the Contact.tsx component, notably:

npx shadcn-ui@latest add toast
Enter fullscreen mode Exit fullscreen mode

Ensure you have imported:

import { useToast } from "~/components/ui/use-toast";

Inside your Contact component, desconstruct useToast

const { toast } = useToast();

Inside your onSubmit handler, add the toast when the email submission is successful, or when it fails, inside the then() clause.

        .then(
          () => {
            console.info('SUCCESS');
            toast({
              title: "Email sent.",
              description: `Thanks ${data.username}, I'll be in touch.`,
            });
            form.reset(); //clear the fields after submission
          },
          (error) => {
            toast({
              variant: "destructive",
              title: "Email failed to send.",
              description: `Please contact support if this continues.`,
            });
            console.warn("FAILED...", JSON.stringify(error));
          },
        );
Enter fullscreen mode Exit fullscreen mode

Lastly, Toast needs to be referenced inside your layout.tsx - if you're not seeing the toast notification on your screen, this is likely the cause.

import { Toaster } from "~/components/ui/toaster";
//...
    <html lang="en">
      <body>
          <Toaster /> // Reference toaster
      </body>
    </html>
Enter fullscreen mode Exit fullscreen mode

Conclusion

And there you have it, with a few tweaks of the default code provided by EmailJS and ShadCN's Form component, you are up and running.

I have provided the entire code here in my GitHub Gist

If you have any thoughts, requests, or questions - let me know. Or, if you would like a complete guide from setting up a new project with all of these tools configured - leave a comment below.

Top comments (0)