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