Remix Validated Form

v6 is coming!

Check out the RFC to get an early look or leave a comment.

(fieldErrors: ValidatorError, submittedData?: unknown, init?: ResponseInit) => Response

Accepts the errors returned from validator.validate and returns a response. The ValidatedForm on the page will automatically receive these errors and display them.

Most of the time, your code will look like this:

const action = async ({ request }: DataFunctionArgs) => {
  const result = await validator.validate(
    await request.formData()
  );
  if (result.error) return validationError(result.error);
  // do something with result.data
};

Repopulating form data

You can also pass a second argument to validationError to repopulate the form with the data that was submitted in cases where the user has javascript disabled.

Caveat: Repopulating the form sets new default values. If the form has a reset button, clicking it will reset the form to the repopulated data, not the original default values. If supporting the no-javascript case is not important to you, you might be better off omitting the second argument.

Caveat 2: If your schema is doing transformations on the data after its validated, you may be opting into extra work for supporting defaultValues. Normally, when you pass defaultValues to a ValidatedForm, the type of that data is the final, validated, and transformed version of your data. But when you return result.submittedData, you're returning the original data (with the nested object and array transformations applied).

In some cases, you might not want to return all the data the user submitted as part of your response. In those cases, you can omit some fields or not pass a second argument at all.

// Omitting some fields
const { password, confirmPassword, ...rest } =
  result.submittedData;
return validationError(result.error, rest);

// Omitting the second argument
return validationError(result.error);

Supporting additional validations

Note: Most use-cases for this can be better solved using async validation.

Sometimes you might want to run some additional checks on the data beyond what the validator can do. For example, if you have a signup form, you might want to check if a username already exists in the database before allowing the user to be created. In those cases, you can manually construct a ValidatorError object.

const action = async ({ request }: DataFunctionArgs) => {
  // We should check the correctness of the data first,
  // before doing our custom check
  const result = await validator.validate(
    await request.formData()
  );
  if (result.error) return validationError(result.error);

  const { username } = result.data;

  // The implementation of `userExistsInDatabase` will
  // vary depending on your own database solution
  if (await userExistsInDatabase(username)) {
    return validationError(
      {
        fieldErrors: {
          username: "This username is already taken",
        },
        // 🚨 Important!
        // If you're using the `subaction` prop,
        // you need to specify a subaction here
        // or else the errors won't be displayed.
        subaction: result.data.subaction,
        // You can also provide a `formId` instead of a `subaction`
        // if your form has an `id` prop
        formId: result.formId,
      },
      result.data
    );
  }

  // Create the user
};

The reason we need to return the submitted data is to improve user experience when javascript is disabled. Normally, if the server returns a validation error and javascript is disabled on the client, the form will be completely reset and the user will have to fill the whole thing out again. By providing the submitted data, the ValidatedForm will set the defaultValues of the form to be the data that was submitted. This way, the user doesn't have to fill out the form again.