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}