Guides / Validation

Validation

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

#Server Validation

You can validate a form fully server side. It is not limited to form submission but also works when user is typing or leaving a field. This allows you to exclude the validation logic from the client bundle. But network latency might be a concern if you want to validate while user is typing.

1import { useForm } from '@conform-to/react';
2import { parseWithZod } from '@conform-to/zod';
3import { z } from 'zod';
4
5export async function action({ request }: ActionArgs) {
6  const formData = await request.formData();
7  const submission = parseWithZod(formData, {
8    schema: z.object({
9      email: z.string().email(),
10      message: z.string().max(100),
11    }),
12  });
13
14  if (submission.status !== 'success') {
15    return submission.reply();
16  }
17
18  return await signup(data);
19}
20
21export default function Signup() {
22  // Last result returned by the server
23  const lastResult = useActionData<typeof action>();
24  const [form] = useForm({
25    // Sync the result of last submission
26    lastResult,
27
28    // Configure when each field should be validated
29    shouldValidate: 'onBlur',
30    shouldRevalidate: 'onInput',
31  });
32
33  // ...
34}

#Client Validation

You can always reuse the validation logic on the client side for instant feedback.

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

#Async Validation

Conform supports async validation in a slightly different way. Instead of sending a request to another endpoint, we will simply fallback to server validation when needed.

Here is an example which validates if the email is unique.

1import { refine } from '@conform-to/zod';
2
3// Instead of sharing a schema, prepare a schema creator
4function createSchema(
5  options?: {
6    isEmailUnique: (email: string) => Promise<boolean>;
7  },
8) {
9  return z
10    .object({
11      email: z
12        .string()
13        .email()
14        // Pipe the schema so it runs only if the email is valid
15        .pipe(
16          // Note: The callback cannot be async here
17          // As we run zod validation synchronously on the client
18          z.string().superRefine((email, ctx) => {
19            // This makes Conform to fallback to server validation
20            // by indicating that the validation is not defined
21            if (typeof options?.isEmailUnique !== 'function') {
22              ctx.addIssue({
23                code: 'custom',
24                message: conformZodMessage.VALIDATION_UNDEFINED,
25                fatal: true,
26              });
27              return;
28            }
29
30            // If it reaches here, then it must be validating on the server
31            // Return the result as a promise so Zod knows it's async instead
32            return options.isEmailUnique(email).then((isUnique) => {
33              if (!isUnique) {
34                ctx.addIssue({
35                  code: 'custom',
36                  message: 'Email is already used',
37                });
38              }
39            });
40          }),
41        ),
42    }),
43    // ...
44}
45
46export function action() {
47	const formData = await request.formData();
48	const submission = await parseWithZod(formData, {
49		// create the zod schema with `isEmailUnique()` implemented
50		schema: createSchema({
51			async isEmailUnique(email) {
52				// ...
53			},
54		}),
55
56		// Enable async validation on the server
57    // We won't set `async: true` on the client
58    // as client validation must be synchronous
59		async: true,
60	});
61
62	// ...
63}
64
65export default function Signup() {
66	const lastResult = useActionData();
67	const [form] = useForm({
68		lastResult,
69		onValidate({ formData }) {
70			return parseWithZod(formData, {
71				// Create the schema without implementing `isEmailUnique()`
72				schema: createSchema(),
73			});
74		},
75	});
76
77	// ...
78}

#Skipping Validation

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

1import { parseWithZod, conformZodMessage } from '@conform-to/zod';
2
3function createSchema(
4  // The `intent` will be provieded by the `parseWithZod` helper
5  intent: Intent | null,
6  options?: {
7    isEmailUnique: (email: string) => Promise<boolean>;
8  },
9) {
10  return z
11    .object({
12      email: z
13        .string()
14        .email()
15        .pipe(
16          z.string().superRefine((email, ctx) => {
17            const isValidatingEmail =
18              intent === null ||
19              (intent.type === 'validate' && intent.payload.name === 'email');
20
21            // This make Conform to use the previous result instead
22            // by indicating that the validation is skipped
23            if (!isValidatingEmail) {
24              ctx.addIssue({
25                code: 'custom',
26                message: conformZodMessage.VALIDATION_SKIPPED,
27              });
28              return;
29            }
30
31            if (typeof options?.isEmailUnique !== 'function') {
32              ctx.addIssue({
33                code: 'custom',
34                message: conformZodMessage.VALIDATION_UNDEFINED,
35                fatal: true,
36              });
37              return;
38            }
39
40            return options.isEmailUnique(email).then((isUnique) => {
41              if (!isUnique) {
42                ctx.addIssue({
43                  code: 'custom',
44                  message: 'Email is already used',
45                });
46              }
47            });
48          }),
49        ),
50    }),
51    // ...
52}
53
54export async function action({ request }: ActionArgs) {
55	const formData = await request.formData();
56	const submission = await parseWithZod(formData, {
57		// Retrieve the intent by providing a function instead
58		schema: (intent) =>
59			createSchema(intent, {
60				async isEmailUnique(email) {
61					// ...
62				},
63			}),
64
65		async: true,
66	});
67
68	// ...
69}
70
71export default function Signup() {
72	const lastResult = useActionData();
73	const [form] = useForm({
74		lastResult,
75		onValidate({ formData }) {
76			return parseWithZod(formData, {
77				// Similar to the action above
78				schema: (intent) => createSchema(intent),
79			});
80		},
81	});
82
83	// ...
84}