Using Remix, Remix Validated Form, Zod, and Zod Form Data, developers can validate forms, easily handle and show error states, prevent false submissions, improve the developer experience, and increase developer velocity. Remix takes advantage of the web platform APIs, which makes it easy to create forms that submit data to an action that passes the form data to the server. Zod provides a way to validate data on both the client and server side, ensuring accuracy and security when handling user data. Zod Form Data further simplifies the process by providing a convenient way to map data from a form to a Zod type. You’ll find the source code and a link to all the packages at the bottom of the post. Another awesome thing about this combination is a lot of functionality still works even if you were to disable JavaScript since validation is handled client and server-side.
Zod is a JavaScript library that provides a powerful, type-safe way to define, validate, and manipulate data. It uses a declarative syntax similar to TypeScript, making it easy to create type-safe forms with React. The library provides a way to validate data on both the client and server side, ensuring accuracy and security when handling user data. Zod Form Data further simplifies the process by providing a convenient way to map data from a form to a Zod type.
The first step in creating a type-safe form is to create a submit button component. This submit button can be handled automatically using the
import { useIsSubmitting } from "remix-validated-form";
export const SubmitButton = ({
submitText = "Submit",
}: {
submitText?: string;
}) => {
const isSubmitting = useIsSubmitting();
return (
<button
type="submit"
disabled={isSubmitting}
className="bg-black text-white p-3 rounded-md"
>
{isSubmitting ? "Submitting..." : submitText}
</button>
);
};
Next, we need to create an input field that uses the
import classNames from "classnames";
import { useField } from "remix-validated-form";
export const Input = ({
name,
title,
id,
}: {
name: string;
title?: string;
id?: string;
}) => {
const field = useField(name);
return (
<div className={"flex flex-col w-full"}>
<label htmlFor={name}>{title}</label>
<input
{...field.getInputProps()}
className={classNames("border-2 rounded-md", {
"border-2 !border-red-500": field.error,
})}
name={name}
id={id ? id : name}
onClick={() => {
field.clearError();
}}
onChange={() => {
if (field.error) field.clearError();
}}
/>
<div className="text-red-500">{field.error}</div>
</div>
);
};
In the below code we create, a schema, we create a validator based on that schema, then we create a client side form where we pass it the schema, the form will then validate the form on both the client and server against that validator. Then once we submit that form data to the server it will validate that data against the validator and then we can pull the validated data.
import { ActionArgs } from "@remix-run/node";
import { withZod } from "@remix-validated-form/with-zod";
import { ValidatedForm, validationError } from "remix-validated-form";
import { z } from "zod";
import { zfd } from "zod-form-data";
import { Input } from "~/components/input";
import { SubmitButton } from "~/components/submit-button";
const createPostSchema = zfd.formData({
// zfd(zod form data) is a helper that helps to parse the form data to an object
// using the zod schema, if there are multiple values with the same name an array will be returned.
// it can handle URLSearchParams, FormData, and plain objects
title: zfd.text(z.string().min(1).max(100)),
author: zfd.text(z.string().min(1).max(50)),
content: zfd.text(z.string().min(1).max(1000)),
published: zfd.checkbox(),
});
export type CreatePostType = z.infer<typeof createPostSchema>;
// remix-validated-form with-zod is a helper that helps to validate form data
// remix-validated-form supported custom validation and other libraries like yup
const createPostValidator = withZod(createPostSchema);
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const validation = await createPostValidator.validate(formData);
// if there are any errors, return validationError, this is also handled
// by remix-validated-form
if (validation.error) {
return validationError(validation.error);
}
// if we make it here, we know that there are no errors so we can
// get the data from the validation object
const { title, content, author, published } = validation.data;
console.log("Creating Post...", { title, content, author, published });
}
export default function () {
return (
<div className="flex items-center justify-center">
{/* Validated form will validate form on both the server side and client side
form will not submit to server if there are any errors.*/}
<ValidatedForm
validator={createPostValidator}
className="flex flex-col space-y-4 w-10/12 lg:w-1/2"
method="post"
>
<Input name="title" title="Post Title" />
<Input name="author" title="Author" />
<Input name="content" title="Post Content" />
<div className="flex flex-row items-center">
<label htmlFor="publish">Publish</label>
<input
type="checkbox"
id="publish"
name="publish"
className="ml-2 h-5 w-5"
/>
</div>
<div className="w-full flex justify-center items-center">
<SubmitButton submitText="Create Post" />
</div>
</ValidatedForm>
</div>
);
}
Here is what our form looks like:
In the below screenshot, I have submitted the form without some of the fields filled out. You’ll notice author field has focus styles, the nice thing about
Let’s break down each piece from the file above, we can break it down into 3 pieces in the order that everything happens:
The
const createPostSchema = zfd.formData({
title: zfd.text(z.string().min(1).max(100)),
author: zfd.text(z.string().min(1).max(50)),
content: zfd.text(z.string().min(1).max(1000)),
published: zfd.checkbox(),
});
const createPostValidator = withZod(createPostSchema);
All we need to do is use the
export default function () {
return (
<div className="flex items-center justify-center
<ValidatedForm
validator={createPostValidator}
className="flex flex-col space-y-4 w-10/12 lg:w-1/2"
method="post"
>
<Input name="title" title="Post Title" />
<Input name="author" title="Author" />
<Input name="content" title="Post Content" />
<div className="flex flex-row items-center">
<label htmlFor="publish">Publish</label>
<input
type="checkbox"
id="publish"
name="publish"
className="ml-2 h-5 w-5"
/>
</div>
<div className="w-full flex justify-center items-center">
<SubmitButton submitText="Create Post" />
</div>
</ValidatedForm>
</div>
);
}
When the form is submitted the action is run on the server side, here we get the form data from the request. We pass that form data to the validator which will check each field against the validation for each field we define. If there is an error then we respond with
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const validation = await createPostValidator.validate(formData);
if (validation.error) {
return validationError(validation.error);
}
const { title, content, author, published } = validation.data;
console.log("Creating Post...", { title, content, author, published });
}
Below we are using Zod’s
const createPostSchema = zfd.formData({
title: zfd.text(z.string().min(1).max(100)),
author: zfd.text(z.string().min(1).max(50)),
content: zfd.text(z.string().min(1).max(1000)),
published: zfd.checkbox(),
});
type CreatePostType = z.infer<typeof createPostSchema>
The resulting type:
Getting forms right and providing a positive user experience is always a challenge, I’ve been playing around with Zod and these libraries for a few months now and I have not found a more simple types, validation, and robust solution than these libraries. I’m also sure I am just barely scratching the surface of what you can do with these libraries.
Shoutout to the packages I used, please check them out and read their docs.
Zod
Remix Validated Form, @remix-validated-form/with-zod, zod-form-data
Source code: