skip to Main Content

I am using react Typescript to build a form with Zod validation schema.

What I am trying to achieve?
So, I want to render conditions based on the values of another field. Every country has its own phone code and phone number format. Every time someone selects a phone code, I want the phone number field to have different validation rules. Here is what I have so far. This is basically a replicate of my actual project.

import { Formik, Field, Form, FormikHelpers } from 'formik'
import { toFormikValidationSchema } from 'zod-formik-adapter'
import { z } from 'zod'
import './App.css'


const FormField = z.object({
  firstName: z.string().max(6, 'Too long'),
  lastName: z.string(),
  email: z.string(),
  phoneCode: z.string(),
  phoneNumber: z.string()
})

const Register = FormField.required()
.refine((input) => {
  return input.phoneCode === '0044';
}, 'Phone code must be 0044');


Register.parse({
  firstName: 'fewfwf',
  lastName: 'Doe',
  email: '[email protected]',
  phoneCode: '0044',
  phoneNumber: '0044'
})

type Values = z.infer<typeof Register>

function App() {

  return (
    <>
      <Formik
        initialValues={{
          firstName: '',
          lastName: '',
          email: '',
          phoneCode: '',
          phoneNumber: ''
        }}
        onSubmit={(
          values: Values,
          { setSubmitting }: FormikHelpers<Values>
        ) => {
          setTimeout(() => {
            alert(JSON.stringify(values, null, 2));
            setSubmitting(false);
          }, 500);
        }}
        validationSchema={toFormikValidationSchema(FormField)}
      >
        {({ errors, touched }) => (
          <Form>
             {errors.firstName ? (<div>{errors.firstName}</div>) : null}
            <label htmlFor="firstName">First Name</label>
            <Field id="firstName" name="firstName" placeholder="John" />

            <label htmlFor="lastName">Last Name</label>
            <Field id="lastName" name="lastName" placeholder="Doe" />

            <label htmlFor="email">Email</label>
            <Field
              id="email"
              name="email"
              placeholder="[email protected]"
              type="email"
            />

            <label htmlFor="phoneCode">PhoneCode</label>
            <Field id="phoneCode" name="phoneCode" placeholder="+44" />

            {errors.phoneNumber && touched.phoneNumber ? (<div>{errors.phoneNumber}</div>) : null}
            <label htmlFor="phoneNumber">PhoneNumber</label>
            <Field id="phoneNumber" name="phoneNumber" placeholder="789434556" />

            <button type="submit">Submit</button>
          </Form>
        )}
      </Formik>

    </>
  )
}

export default App

As you can see, I have used .refine() and add some random condition to test. When I run this through the parse method, it validates correctly and works, however, when I submit the form from the frontend, it still executes, even if phoneCode is not equal to ‘0044’. Is this normal behavior for the .refine method? Is there another approach that would be more practical?

3

Answers


  1. Chosen as BEST ANSWER

    I think I managed to solve this issue, thanks to the last post by Sina Ghandri. The idea to useFormikContext worked. I combined this with useState to store the value and then it with function to getPhoneNumberSchema to conditionally render a custom validation for phoneNumber. Here is the code in case it helps anyone else in the future

    import { Formik, Field, Form, FormikHelpers, useFormikContext } from 'formik'
    import { toFormikValidationSchema } from 'zod-formik-adapter'
    import { useEffect, useState }  from 'react'
    import { z } from 'zod'
    import './App.css'
    
    const FormField = z.object({
      firstName: z.string().max(6, 'Too long'),
      lastName: z.string(),
      email: z.string(),
      phoneCode: z.string(),
      phoneNumber: z.string()
    })
    
    FormField.parse({
      firstName: 'fewfwf',
      lastName: 'Doe',
      email: '[email protected]',
      phoneCode: '0044',
      phoneNumber: '0044'
    })
    
    const getPhoneNumberSchema = (phoneCode: string) => {
      switch (phoneCode) {
        case '0044':
          return z.string().length(10, 'UK phone number must be 10 digits long');
        // Add cases for other phone codes with their respective validation rules
        default:
          return z.string();
      }
    };
    
    type Values = z.infer<typeof FormField>
    
    function App() {
    
      const [phoneCode, setPhoneCode] = useState('');
    
      const Logger = () => {
        const formik = useFormikContext<Values>();
        useEffect(() => {
          console.log("values", formik.values.phoneCode)
          setPhoneCode(formik.values.phoneCode)
        }, [formik.values.phoneCode])
        return null;
      }
      
      return (
        <>
          <Formik
            initialValues={{
              firstName: '',
              lastName: '',
              email: '',
              phoneCode: '',
              phoneNumber: ''
            }}
            onSubmit={(
              values: Values,
              { setSubmitting }: FormikHelpers<Values>
            ) => {
              setTimeout(() => {
                alert(JSON.stringify(values, null, 2));
                setSubmitting(false);
              }, 500);
            }}
            validationSchema={toFormikValidationSchema(FormField.extend({
              phoneNumber: getPhoneNumberSchema(phoneCode)
            }))}
          >
            {({ errors, touched }) => (
              <Form>
                <Logger />
                 {errors.firstName ? (<div>{errors.firstName}</div>) : null}
                <label htmlFor="firstName">First Name</label>
                <Field id="firstName" name="firstName" placeholder="John" />
    
                <label htmlFor="lastName">Last Name</label>
                <Field id="lastName" name="lastName" placeholder="Doe" />
    
                <label htmlFor="email">Email</label>
                <Field
                  id="email"
                  name="email"
                  placeholder="[email protected]"
                  type="email"
                />
    
                <label htmlFor="phoneCode">PhoneCode</label>
                <Field id="phoneCode" name="phoneCode" placeholder="+44" />
    
                {errors.phoneNumber && touched.phoneNumber ? (<div>{errors.phoneNumber}</div>) : null}
                <label htmlFor="phoneNumber">PhoneNumber</label>
                <Field id="phoneNumber" name="phoneNumber" placeholder="789434556" />
    
                <button type="submit">Submit</button>
              </Form>
            )}
          </Formik>
    
        </>
      )
    }
    
    export default App
    

  2. The .refine() method in Zod is used to apply custom refinement checks during the parsing phase. It ensures that the input satisfies the specified condition and throws an error if it does not. However, when you are using Formik and Zod together, you might want to handle these dynamic validation rules differently.

    In your case, you want to conditionally validate the phoneNumber field based on the selected phoneCode value. To achieve this, you can use the refine method on the individual fields within the validation schema and perform the conditional check inside the refine function. However, keep in mind that the refinement function is evaluated during the parsing phase and does not automatically get applied in the Formik context

    import { Formik, Field, Form, FormikHelpers } from 'formik';
    import { toFormikValidationSchema } from 'zod-formik-adapter';
    import { z } from 'zod';
    import './App.css';
    
    const FormField = z.object({
      firstName: z.string().max(6, 'Too long'),
      lastName: z.string(),
      email: z.string(),
      phoneCode: z.string(),
      phoneNumber: z
        .string()
        .refine((value, data) => {
          // Perform your dynamic validation based on phoneCode
          return data.phoneCode === '0044' ? value.length === 10 : true;
        }, 'Invalid phone number length'),
    });
    
    type Values = z.infer<typeof FormField>;
    
    function App() {
      return (
        <>
          <Formik
            initialValues={{
              firstName: '',
              lastName: '',
              email: '',
              phoneCode: '',
              phoneNumber: '',
            }}
            onSubmit={(
              values: Values,
              { setSubmitting }: FormikHelpers<Values>,
            ) => {
              setTimeout(() => {
                alert(JSON.stringify(values, null, 2));
                setSubmitting(false);
              }, 500);
            }}
            validationSchema={toFormikValidationSchema(FormField)}
          >
            {({ errors, touched }) => (
              <Form>
                {errors.firstName ? <div>{errors.firstName}</div> : null}
                <label htmlFor="firstName">First Name</label>
                <Field id="firstName" name="firstName" placeholder="John" />
    
                <label htmlFor="lastName">Last Name</label>
                <Field id="lastName" name="lastName" placeholder="Doe" />
    
                <label htmlFor="email">Email</label>
                <Field
                  id="email"
                  name="email"
                  placeholder="[email protected]"
                  type="email"
                />
    
                <label htmlFor="phoneCode">PhoneCode</label>
                <Field id="phoneCode" name="phoneCode" placeholder="+44" />
    
                {errors.phoneNumber && touched.phoneNumber ? (
                  <div>{errors.phoneNumber}</div>
                ) : null}
                <label htmlFor="phoneNumber">PhoneNumber</label>
                <Field
                  id="phoneNumber"
                  name="phoneNumber"
                  placeholder="789434556"
                />
    
                <button type="submit">Submit</button>
              </Form>
            )}
          </Formik>
        </>
      );
    }
    
    export default App;

    In this way, the .refine() method is used on the phoneNumber field to perform the dynamic validation based on the phoneCode. Adjust the condition inside the refine function according to your specific validation logic.

    This way, the validation will be applied during the parsing phase, and you’ll see the error message if the condition is not met.

    Login or Signup to reply.
  3. You can modify your existing code to include this dynamic validation:

    Create a Function for Dynamic Phone Number Validation:
    Define a function that returns a Zod schema for the phone number based on the selected phone code.

    Update the Formik Validation Schema:
    Use the dynamic phone number validation function within your Formik validation schema.

    Update the Form Component:
    Use Formik’s context to access the current value of the phone code and apply the corresponding validation schema to the phone number field.

    import { Formik, Field, Form, FormikHelpers, useFormikContext } from 'formik';
    import { toFormikValidationSchema } from 'zod-formik-adapter';
    import { z } from 'zod';
    import './App.css';
    
    // Function to get dynamic phone number validation schema based on phone code
    const getPhoneNumberSchema = (phoneCode: string) => {
      switch (phoneCode) {
        case '0044':
          return z.string().length(10, 'UK phone number must be 10 digits long');
        // Add cases for other phone codes with their respective validation rules
        default:
          return z.string();
      }
    };
    
    const FormField = z.object({
      firstName: z.string().max(6, 'Too long'),
      lastName: z.string(),
      email: z.string().email(),
      phoneCode: z.string(),
      phoneNumber: z.string() // Default validation, will be dynamically updated
    });
    
    type Values = z.infer<typeof FormField>;
    
    function App() {
      return (
        <>
          <Formik
            initialValues={{
              firstName: '',
              lastName: '',
              email: '',
              phoneCode: '',
              phoneNumber: ''
            }}
            onSubmit={(values: Values, { setSubmitting }: FormikHelpers<Values>) => {
              setTimeout(() => {
                alert(JSON.stringify(values, null, 2));
                setSubmitting(false);
              }, 500);
            }}
            validationSchema={() => {
              const { values } = useFormikContext<Values>();
              return toFormikValidationSchema(FormField.extend({
                phoneNumber: getPhoneNumberSchema(values.phoneCode)
              }));
            }}
          >
            {({ errors, touched }) => (
              <Form>
                {/* ... other fields ... */}
                <Field id="phoneCode" name="phoneCode" placeholder="+44" />
                <Field id="phoneNumber" name="phoneNumber" placeholder="789434556" />
                {errors.phoneNumber && touched.phoneNumber && <div>{errors.phoneNumber}</div>}
                <button type="submit">Submit</button>
              </Form>
            )}
          </Formik>
        </>
      )
    }
    
    export default App;
    

    The getPhoneNumberSchema function returns a different Zod schema for the phone number field depending on the selected phone code.
    The Formik validationSchema is dynamically generated using the current form values, allowing the phone number validation to adapt based on the selected phone code.
    Make sure to handle different phone codes and their respective validation rules within the getPhoneNumberSchema function.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search