Client-side validation in Remix with Zod and clientActions

Exploring a novel method for client-side form validation in Remix using Zod and clientActions

September 22, 2024

Form validation is an essential part of web development for security, data integrity and UX. Remix offers incredibly simple means for handling forms but the guides around validating these forms leave a lot to be desired. The official documentation and other resources propose manual validation of each field in a route's action handler, which can get pretty cumbersome if you have a large or complex form.

This is an example from the official documentation where a simple email and password form is submitted and validated on the server.

export async function action({ request }) { const formData = await request.formData() const email = String(formData.get('email')) const password = String(formData.get('password')) const errors = {} if (!email.includes('@')) { errors.email = 'Invalid email address' } if (password.length < 12) { errors.password = 'Password should be at least 12 characters' } if (Object.keys(errors).length > 0) { return json({ errors }) } return redirect('/dashboard') }

This will work for basic cases, but as your business requirements change over time you may add additional fields to this form or require a more complex password schema.

Rather than defining more rules as if-statements, a popular solution is to use something like Zod. Zod allows you to define a schema with all of your rules so can validate input at runtime.

Below is the same example as above but with a Zod schema instead of manual field validation. You can see how much more robust this flow would be if we were to have password rules like requiring special characters, or numbers.

const loginSchema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(12, 'Password should be at least 12 characters'), }) export async function action({ request }) { const formData = await request.formData() const rawFormData = Object.fromEntries(formData) const result = loginSchema.safeParse(rawFormData) if (result.success) { return redirect('/dashboard') } else { return json({ errors: result.error.flatten().fieldErrors }) } }

However, this validation still occurs entirely server-side. We have to make a round-trip to the server every time we want to validate the form and potentially return error messages. What if we could perform this validation entirely on the client-side, without any additional libraries, preventing unnecessary server requests altogether? Enter Remix's clientActions.

clientAction

clientAction's are optional handlers that run client-side before a network request is made to your server action. The API is almost identical to the existing server action you may be familiar with, with the addition of a serverAction property which is a method used to forward the request on.

The similarities of the API mean we can apply our Zod schema validation on the client-side using the same approach we use server-side.

const loginSchema = z.object({ // unchanged }) export async function action({ request }) { // unchanged } export async function clientAction({ request, serverAction }) { // Clone request so the body can be reused in the action const clonedRequest = request.clone() const formData = await clonedRequest.formData() const rawFormData = Object.fromEntries(formData) const result = loginSchema.safeParse(rawFormData) if (result.success) { // If validation passes, forward the request to the server action return serverAction() } else { return { errors: result.error.flatten().fieldErrors } } }

This extremely simple approach comes with some major benefits:

  • Users get instant feedback on their input without having to wait for around-trip to the server
  • The server doesn't waste resources validating forms that we already know are invalid
  • The form validation schema can be defined once and reused used both client-side and server-side. This means no code duplication and guaranteed sync of validation rules in both places.

Wrapping up

Performing client-side validation like this is an easy win as it comes out of the box with Remix. You technically don't even need Zod for this and could achieve the same by encapsulating your validation logic into a reusable method and calling it both action and clientAction.

It's worth mentioning here that this is not a replacement for server-side validation and you should always validate there before passing user input to your backend code.

It's also worth mentioning that are dedicated libraries for integrating Zod client-side, such as Conform, remix-hook-form and Remix Validated Form, that offer more fine-grained control over the UX of the form and error messages. However, if you just wish to validate form input against a Zod schema, I highly recommend this simple approach.

me
Brian BriscoeCreator of Launchway