Validation

Conform supports several validation modes. In this section, we will walk you through how to validate a form based on different requirements.

Server Validation

Conform enables you to validate a form fully server side.

1import { useForm } from '@conform-to/react';
2import { parse } from '@conform-to/zod';
3import { z } from 'zod';
4
5export async function action({ request }: ActionArgs) {
6  const formData = await request.formData();
7  const submission = parse(formData, {
8    schema: z.object({
9      email: z
10        .string({ required_error: 'Email is required' })
11        .email('Email is invalid'),
12      message: z
13        .string({ required_error: 'Message is required' })
14        .max(100, 'Message is too long'),
15    }),
16  });
17
18  if (submission.intent !== 'submit' || !submission.value) {
19    return json(submission);
20  }
21
22  return await signup(data);
23}
24
25export default function Signup() {
26  // Last submission returned by the server
27  const lastSubmission = useActionData<typeof action>();
28  const [form] = useForm({
29    // Sync the result of last submission
30    lastSubmission,
31  });
32
33  // ...
34}
35

Client Validation

Server validation works well generally. However, network latency would be a concern if there is a need to provide instant feedback while user is typing. In this case, you might want to validate on the client side as well.

1import { useForm } from '@conform-to/react';
2import { parse } from '@conform-to/zod';
3
4// Move the schema definition out of action
5const schema = z.object({
6  email: z
7    .string({ required_error: 'Email is required' })
8    .email('Email is invalid'),
9  message: z
10    .string({ required_error: 'Message is required' })
11    .max(100, 'Message is too long'),
12});
13
14export async function action({ request }: ActionArgs) {
15  const formData = await request.formData();
16  const submission = parse(formData, { schema });
17
18  // ...
19}
20
21export default function Signup() {
22  const lastSubmission = useActionData<typeof action>();
23  const [form] = useForm({
24    lastSubmission,
25
26    // Setup client validation
27    onValidate({ formData }) {
28      return parse(formData, { schema });
29    },
30  });
31
32  // ...
33}
34

Async Validation

Here is an example how you can do async validation with zod:

1import { refine } from '@conform-to/zod';
2
3// Instead of reusing a schema, let's prepare a schema creator
4function createSchema(constraint?: {
5  isEmailUnique?: (email) => Promise<boolean>;
6}) {
7  return z.object({
8    email: z
9      .string({ required_error: 'Email is required' })
10      .email('Email is invalid')
11      // Pipe another schema so it runs only if the email is valid
12      .pipe(
13        z.string().superRefine((email, ctx) =>
14          // Using the `refine` helper from Conform
15          refine(ctx, {
16            validate: () => constraint.isEmailUnique?.(email),
17            message: 'Username is already used',
18          }),
19        ),
20      ),
21    // ...
22  });
23}
24
25export function action() {
26  const formData = await request.formData();
27  const submission = await parse(formData, {
28    // create the zod schema with `isEmailUnique()` implemented
29    schema: createSchema({
30      async isEmailUnique(email) {
31        // ...
32      },
33    }),
34
35    // Enable async validation
36    async: true,
37  });
38
39  // ...
40}
41
42export default function Signup() {
43  const lastSubmission = useActionData();
44  const [form] = useForm({
45    lastSubmission,
46    onValidate({ formData }) {
47      return parse(formData, {
48        // Create the schema without implementing `isEmailUnique()`
49        schema: createSchema(),
50      });
51    },
52  });
53
54  // ...
55}
56

Skipping Validation

Conform validates all fields by default. This could be expensive especially with async validation. One solution is to minimize the validation by checking the submission intent.

1import { parse } from '@conform-to/zod';
2
3function createSchema(
4  // Accept an intent on the schema creator
5  intent: string,
6  options?: {
7    isEmailUnique?: (email) => Promise<boolean>;
8  },
9) {
10  return z.object({
11    email: z
12      .string({ required_error: 'Email is required' })
13      .email('Email is invalid')
14      .pipe(
15        z.string().superRefine((email, ctx) =>
16          refine(ctx, {
17            validate: () => constraint.isEmailUnique?.(email),
18            // Check only when it is validating the email field or submitting
19            when: intent === 'submit' || intent === 'validate/email',
20            message: 'Username is already used',
21          }),
22        ),
23      ),
24    // ...
25  });
26}
27
28export async function action({ request }: ActionArgs) {
29  const formData = await request.formData();
30  const submission = await parse(formData, {
31    // Retrieve the intent by providing a function instead
32    schema: (intent) =>
33      createSchema(intent, {
34        async isEmailUnique(email) {
35          // ...
36        },
37      }),
38
39    async: true,
40  });
41
42  // ...
43}
44
45export default function Signup() {
46  const lastSubmission = useActionData();
47  const [form] = useForm({
48    lastSubmission,
49    onValidate({ formData }) {
50      return parse(formData, {
51        // Similar to the action above
52        schema: (intent) => createSchema(intent),
53      });
54    },
55  });
56
57  // ...
58}
59