Form validation

You can validate forms in two places: the client and the server.

We recommend doing both for different reasons:

  1. Validate fields on the client for a better user experience, field errors are displayed immediately
  2. Validate on the server for security, to prevent malicious users from submitting invalid data

Client-side validation

All form input components support client-side validation via the validate prop.

<Form>
  <TextField
    id="username"
    name="username"
    label="Username"
    validate={(value) => {
      if (!value) {
        return 'Please enter your username';
      }
    }}
  />
</Form>

The validate function is called anytime the value changes. The error message returned from validate will be displayed underneath the input when the form is submitted.

Please enter your username

Field error summary

Individual input fields will display their own validation errors inline. However, this approach does not give a very good experience to screen reader users. For screen readers it's better to display a summary of field errors in a single place so that they can see a high-level summary of all the errors in one place.

Please enter your username
Please enter your password

This is achieved by rendering a FieldErrorSummary component at the top of the form.

<Form>
  <FormErrors>
    <FieldErrorSummary
      fieldLabels={{ username: 'Username', password: 'Password' }}
      title="There are errors in your submission"
      subtitle="Please correct your input in these fields:"
    />
  </FormErrors>
  <TextField
    id="username"
    name="username"
    label="Username"
    isRequired
    validate={(value) => {
      if (!value) {
        return 'Please enter your username';
      }
    }}
  />
  <TextField
    id="password"
    name="password"
    label="Password"
    isRequired
    validate={(value) => {
      if (!value) {
        return 'Please enter your password';
      }
    }}
  />
  <Button type="submit">Log in</Button>
</Form>

Server-side validation

There are two types of errors that can be returned from a form action:

  1. Server error - An error that occurred on the server, e.g. an API error occurred
  2. Field errors - Validation errors for individual fields, e.g. a required field is not filled in

Server errors

Server errors are returned from the form action when an error occurs on the server, e.g. an API error occurred.

import { FormAction } from '@/ui/components/forms/types';
 
export const loginAction: FormAction = async (currentState, formData) => {
  const values = getFormValues<{ email: string; password: string }>(formData);
 
  try {
    await login(values.email, values.password);
  } catch (error) {
    return {
      status: 'error',
      formData,
      serverError: 'Failed to login',
    };
  }
 
  // ...
};

And then displayed in the form component like this:

const form = useFormAction<{ email: string; password: string }>(loginAction);
 
return (
  <Form action={form.action}>
    <FormErrors>
        <ServerError errorMessage={form.serverError} title="An error occurred" />
    </FormErrors>
  </Form>
);

Field errors

It's better to do all field level validation on the client side, but if you do need to validate fields on the server, you can also return field level errors from the form action.

export const loginAction: FormAction = async (currentState, formData) => {
  const values = getFormValues<{ email: string; password: string }>(formData);
 
  if (!values.email) {
    return {
      status: 'error',
      formData,
      fieldErrors: { email: 'Please enter your email address' },
    };
  }
};

You then need to pass these errors to the form's validationErrors prop:

<Form validationErrors={form.fieldErrors}>
  <FormErrors>
    <FieldErrorSummary fieldLabels={{ email: 'Email' }} />
  </FormErrors>
</Form>
Please enter your email address