DEV Community

Cover image for Form Validation In Remix Using Zod
Ishan Manandhar
Ishan Manandhar

Posted on

Form Validation In Remix Using Zod

Remix is an awesome React framework for building modern SSR (Sever Side Rendering) web experience. Which means we can work with both backend and frontend in a single Remix app. Remix is really unique loaded with full of great features. One of the most distinct ones is when working in forms. Remix brings back the traditional method of handling forms.

Remix provides functions (called action and loaders) that we can use to perform server-side operations and access a form’s data. With these functions, we no longer need to serve JavaScript to the frontend to submit a form, thereby reducing the browser’s javascript chunks.

When we are doing validation one of my personal choice of library is Zod. Zod is a TypeScript-first schema declaration and validation library. With Zod, we declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures.

Why need a validation?

We want the data submitted by the users are safe and as expected. There are three main reason we need a validation login in building our application.

  • We want to get the right data, in the right format. Our applications won't work properly if our users' data is stored in the wrong format, is incorrect, or is omitted altogether.

  • We want to protect our users' data. Forcing our users to enter secure passwords makes it easier to protect their account information.

  • We want to protect ourselves. There are many ways that malicious users can misuse unprotected forms to damage the application.

What are we building

We are building a form validation from scratch in Remix using Zod. There are often times we need to validate our data in the server-side. This is a killer combination we can have so that our data we receive from our API will be fully typed and we only get valid data we need. We will enforce users just to submit data we intend to receive to validate user input in the route, before data gets saved, regardless of where we want to store the data.

Form in Remix

Remix provides a custom Form component we can work identically to the native HTML element. When working with React we needed to listen to the onChange event in all the form fields and update our state. But, instead Remix uses form data from the web's formData() API.

Form is a Remix-aware and enhanced HTML form component which behaves like a normal form except that the interaction with the server is with fetch instead of new document requests. Form will do a POST request to the current page route automatically. However, we can configure it for PUT and DELETE and change as per our need alongside with the action method needed to handle the form requests.

import { Form, useActionData } from '@remix-run/react';

export async function action({ request }) {
  //handle logic with form data and return a value  
}

export default function Index() {
  const actionData = useActionData(); 
//we access the return value of the action with this hook
  return (
    <Form
      method="post">
      //add our form fields here
      <button type="submit">Create Account</button>
    </Form>
  );
}
Enter fullscreen mode Exit fullscreen mode

We are using the built in Remix form component and making the use of useActionData hook. This is a special hook which will help us in send the request (POST in this case) with the form data to the server using the fetchAPI. This returns the JSON parsed data from a route’s action. It is most commonly used when handling form validation errors later.

Adding our form

We can make the use of the Form imported from Remix and use it in our Form. Look at the snippet below how simple it is

<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <div className="max-w-lg w-full space-y-8">
          <div>
            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
              Remix Form Validation with Zod
            </h2>
          </div>
          <Form method="post" noValidate={true}>
            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Full name
                </label>
                <input
                  name="name"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                  {/* print errors here */}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Email
                </label>
                <input
                  name="email"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                      {/* print errors here */}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="omfirm Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Confirm Email
                </label>
                <input
                  name="confirmEmail"
                  type="email"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
              </div>
              <span className="text-sm text-red-500">
                    {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Expertise"
                  className="block text-sm font-medium text-gray-700"
                >
                  Expertise
                </label>
                <select
                  name="expertise"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                >
                  <option></option>
                  <option>Product Designer</option>
                  <option>Frontend Developer</option>
                  <option>Backend Developer</option>
                  <option>Fullstack Developer</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                        {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Github URL
                </label>
                <input
                  name="url"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
              </div>
              <span className="text-sm text-red-500">
                        {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700"
                >
                  Currently Available
                </label>
                <select
                  name="availability"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                >
                  <option></option>
                  <option>Full-time</option>
                  <option>Part-time</option>
                  <option>Contract</option>
                  <option>Freelance</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                       {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Description
                </label>
                <textarea
                  name="description"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
              </div>
              <span className="text-sm text-red-500">
                {/* print errors here */}
              </span>
            </div>

            <div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                Submit
              </button>
            </div>
          </Form>
        </div>
      </div>
Enter fullscreen mode Exit fullscreen mode

We have a basic structure of the form in place, We also have hooked in the button for submit which uses native submit formData() API.

Adding validation logic (with Zod)

When user clicks on the submit button. The action function gets invoked. This is the place where we will add all the logic needed to do the validation needed.

Lets install our library before we can use it with

npm i zod

Enter fullscreen mode Exit fullscreen mode
import { ActionFunction } from '@remix-run/node';
import { z } from 'zod';

export const action: ActionFunction = async ({ request }) => {
  const formPayload = Object.fromEntries(await request.formData());

  const validationSchema = z
    .object({
      name: z.string().min(3),
      email: z.string().email(),
      confirmEmail: z.string().email(),
      expertise: z.enum([
        'Product Designer',
        'Frontend Developer',
        'Backend Developer',
        'Fullstack Developer',
      ]),
      url: z.string().url().optional(),
      availability: z.enum(['Full-time', 'Part-time', 'Contract', 'Freelance']),
      description: z.string().nullable(),
    })
    .refine((data) => data.email === data.confirmEmail, {
      message: 'Email and confirmEmail should be same email',
      path: ['confirmEmail'],
    });

  try {
    const validatedSchema = validationSchema.parse(formPayload);
    console.log('Form data is valid for submission:', validatedSchema); //API call can be made here
  } catch (error) {
    return {
      formPayload,
      error,
    };
  }
  return {} as any;
};

Enter fullscreen mode Exit fullscreen mode

There are couple of things thats going on in the validation logic. We have defined our schema here with z.object({}) method provided to us by Zod. In the given keys we are adding the validation logic as per we desire.

You may have noticed we have covered a wide range of validation which incorporates just string validation, email, minimum character, using enum, url, optional field or nullable. Later we have also used .refine schema method which helps us in adding custom validation logic via refinements.

.refine(validator: (data:T)=>any, params?: RefineParams)

With this we can define a custom validation check in any of the Zod schema. Where we have checked the both email fields needs to match each other. You can find more about this method in Zod documentation here.

We will go ahead and add additional attributes like key and defaultValue in our form fields. Using key={} in the form fields. This is a gotcha to force React to re-render the component. Otherwise, your data might not be updated. This happens because when defaultValue={} is used, creating an uncontrolled component, React will assume that data is immutable and will not re-render the component when the value changes.

Now our form markup would look something like this


export default function Index() {
  const actionData = useActionData();
  return (
    <div>
      <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <div className="max-w-lg w-full space-y-8">
          <div>
            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
              Remix Form Validation with Zod
            </h2>
          </div>
          <Form method="post" noValidate={true}>
            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Full name
                </label>
                <input
                  name="name"
                  type="text"
                  defaultValue={actionData?.formPayload?.name}
                  key={actionData?.formPayload?.name}
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                  {actionData?.error?.issues[0]?.message}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Email
                </label>
                <input
                  name="email"
                  type="text"
                  defaultValue={actionData?.formPayload?.email}
                  key={actionData?.formPayload?.email}
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                  {actionData?.error?.issues[1]?.message}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Confirm Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Confirm Email
                </label>
                <input
                  name="confirmEmail"
                  type="email"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                  defaultValue={actionData?.formPayload?.confirmEmail}
                  key={actionData?.formPayload?.confirmEmail}
                />
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[2]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Expertise"
                  className="block text-sm font-medium text-gray-700"
                >
                  Expertise
                </label>
                <select
                  name="expertise"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  defaultValue={actionData?.formPayload?.expertise}
                  key={actionData?.formPayload?.expertise}
                >
                  <option></option>
                  <option>Product Designer</option>
                  <option>Frontend Developer</option>
                  <option>Backend Developer</option>
                  <option>Fullstack Developer</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[3]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Github URL
                </label>
                <input
                  name="url"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                  defaultValue={actionData?.formPayload?.url}
                  key={actionData?.formPayload?.url}
                />
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[4]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700"
                >
                  Currently Available
                </label>
                <select
                  name="availability"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  defaultValue={actionData?.formPayload?.availability}
                  key={actionData?.formPayload?.availability}
                >
                  <option></option>
                  <option>Full-time</option>
                  <option>Part-time</option>
                  <option>Contract</option>
                  <option>Freelance</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[5]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Description
                </label>
                <textarea
                  name="description"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                  defaultValue={actionData?.formPayload?.description}
                  key={actionData?.formPayload?.description}
                />
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[6]?.message}
              </span>
            </div>

            <div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                Submit
              </button>
            </div>
          </Form>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

We have successfully achieved our form validation. But thing to be noted is we have just done a server side validation but client side still remains. Its best recommended to do the validation in both client and the server so get the data just like we expect from the users. We will set this to our next article.

You can find the source code used in this article in the Github Repository.

Happy Coding!

Discussion (0)