Remix Validated Form

RVF v6 has been released!

For new projects, we recommend starting with v6. For existing projects, there's a migration guide available.

Integration with your components

The apis in this library are designed to be integrated into your form components and minimize the boilerplate of your actual forms. You can take full advantage of the library with just an input and a submit button.

Create an input component

useField lets you integrate field-by-field validation and form-level default values into your inputs. In the example below, we have an input that gets validated automatically, shows validation errors, and gets a default value from ValidatedForm's defaultValues prop.

useField consumes the context provided by ValidatedForm, so these features only work if you use it in a ValidatedForm. However, it is still safe to use your input outside of a ValidatedForm, it just won't be able to take advantage of the features.

If this example doesn't validate at the right times for your use-case or you don't have a plain input, check out the useField documentation to see what other helpers and options are available.

import { useField } from "remix-validated-form";

type MyInputProps = {
  name: string;
  label: string;
};

export const MyInput = ({ name, label }: MyInputProps) => {
  const { error, getInputProps } = useField(name);
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input {...getInputProps({ id: name })} />
      {error && (
        <span className="my-error-class">{error}</span>
      )}
    </div>
  );
};

Create a submit button component

The useIsSubmitting hook enables you to detect if the current form is submitting, even if there are multiple forms on the page.

import { useIsSubmitting } from "remix-validated-form";

export const MySubmitButton = () => {
  const isSubmitting = useIsSubmitting();
  return (
    <button type="submit" disabled={isSubmitting}>
      {isSubmitting ? "Submitting..." : "Submit"}
    </button>
  );
};

Use in a form

Now, we can create a fully validated form with minimal boilerplate.

import { ValidatedForm } from "remix-validated-form";
import { withZod } from "@remix-validated-form/with-zod";
import { z } from "zod";

export const validator = withZod(
  z.object({
    firstName: z
      .string()
      .min(1, { message: "First name is required" }),
    lastName: z
      .string()
      .min(1, { message: "Last name is required" }),
    email: z
      .string()
      .min(1, { message: "Email is required" })
      .email("Must be a valid email"),
  }),
);

export default function MyPage() {
  return (
    <ValidatedForm validator={validator} method="post">
      <FormInput name="firstName" label="First Name" />
      <FormInput name="lastName" label="Last Name" />
      <FormInput name="email" label="Email" />
      <SubmitButton />
    </ValidatedForm>
  );
}