Search code examples
javascriptreactjsviteformikyup

Creating reusable form components using Formik and Yup library for react + vite


I am trying to create a react+vite project where I am trying to implement a reusable form components using Formik and Yup but not able to achieve this yet. I have created a component called <AppInputField/> which is just an <input/> field with custom css to suit my design of the overall website. Similar to this I have <AppButton/> which is again just <button/>. And all the required attributes I am passing through props. Now when I am trying to create a similar for <AppForm/> which will wrap around formik library I am having a hard time solving it. From the searches I have came to know that it is related useFormik() and useFormikContext() but again not 100% sure. Here's my codes :

SignUp.jsx

const SignUp = () => {
  
const signUpSchema = Yup.object({
  name: Yup.string().min(2).max(25).required("Please enter your name"),
  });

  return (
    <>
    <AppForm
        initialValues={{
            name : ''
        }}
        validationSchema={signUpSchema}
        onSubmit={(values) => {console.log("Submitted with these values\n" + values)}}
    >
        <AppInputField 
          name='name'
          label='Enter your name'
          placeholder='XYZ'
        />
        <SubmitButton title='Register'/>
    </AppForm>
    </>
  );
};

export default SignUp;

AppForm.js

function AppForm({initialValues, onSubmit, validationSchema, children}) {
    return (
        <Formik
            initialValues={ initialValues }
            onSubmit={ onSubmit }
            validationSchema={ validationSchema } 
        >
                {children}
                {/* children passed as props here are the inner components of form 
meaning AppInputField and SubmitButton
this actually renders the inside components */}
        </Formik>
    );
}
export default AppForm;

AppInputField.jsx

const AppInputField = ({name, label, placeholder, type='text') => {
    const { handleChange, handleBlur, values } = useFormikContext();
    return (
        <>
        <label htmlFor={name} className="input-label">{label}</label>
        <input
            autoComplete="off"
            className="input-text"
            id={name}
            name={name} 
            onChange={handleChange}
            onBlur={handleBlur}
            placeholder={placeholder}
            type={type}
            value={values[name]}
        />
        </>
    )
}
export default AppInputField

SubmitButton.jsx

function SubmitButton({title}) {
    const { handleSubmit } = useFormikContext();
    return (
        <AppButton 
            title={title} 
            type='submit' 
            onClick={handleSubmit}
        />
    );
}
export default SubmitButton;

So the error when clicking on SubmitButton is Uncaught TypeError: Cannot destructure property 'handleSubmit ' of 'useFormikContext(...)' as it is undefined. If I change it to something submitForm then same error. Also when using the useFormik() the code of the components is changed but there also no success. When using useFormik I cannot use the context in another component. I need to use all in the same form at the same jsx file. Hope i am able to explain my objective. This kind of abstraction I had came across when going through course of react-native of Mosh Hemdani. And I found it so beautiful that I want to achieve this in my react+vite project. Any help would be appreciated.


Solution

  • Got it! would love to share with you all. useField was way to go and thanks to adsy for guiding. Here is my code with Yup and Formik...

    AppForm.jsx

    const AppForm = ({initialValues, validationSchema, onSubmit, children}) => (
      <>
        <Formik
          initialValues={initialValues}
          validationSchema={validationSchema}
          onSubmit={onSubmit}
        >
          {(formikProps) => (
            <Form>
    
    
                {React.Children.map(children, child => {
                    if (React.isValidElement(child)) {
                      React.cloneElement(child);
                    }
                    return child;
                })}
            </Form>
          )}
        </Formik>
      </>
    );
    export default AppForm
    

    AppInputField.jsx

    const AppFormInputField = ({label, type='text', ...props}) => {
        const [field, meta, helpers] = useField(props);
      return (
        <>
            <AppInputField label={label} type={type} {...field} {...props}/>
            <ErrorMessage error={meta.error} visible={meta.touched && meta.error} />
        </>
      )
    }
    
    export default AppFormInputField
    

    AppFormFileUpload.jsx

    const AppFormFileUpload = ({label, allowedExtensions, setFieldValue, ...props}) => {
        const [field, meta, helpers] = useField(props);
      return ( 
        <>
    {/*FileUploadField is basically input field with attribute type=file with custom css*/}
          <FileUploadField label={label} {...field} {...props}  
            value={undefined}
            onChange={(event) => {
              helpers.setValue(event.target.files)
            }}
          />
          <ErrorMessage error={meta.error} visible={meta.touched && meta.error}/>
        </>
      )
    }
    
    export default AppFormFileUpload
    

    No change in SubmitButton.jsx

    SignUp.jsx

    const SignUp = () => {
    
      const checkSchema = Yup.object({
        firstName: Yup.string().min(3).required("First Name is Required"),
        file : Yup.mixed().required('required')
        .test('fileFormat', 'Only approved files are allowed', (value, context) => {
          if (value) {//just pretest check
            const supportedFormats = ['pdf','jpg', 'gif', 'png', 'jpeg', 'svg', 'webp'];
            for (const [key, val] of Object.entries(value)) {
              if(!supportedFormats.includes(val.name.split('.').pop())) return false
            }
          }
          return true;
        })
        .test('fileSize', 'File size must be less than 3MB', (value, context) => {
          if (value) {//just pretest check
            for (const [key, val] of Object.entries(value)) {
              if(val.size > 3145728) return false
            }
          }
          return true;
        }),
      })
    
      const initialValues={
        firstName: '',
        file : ''
      }
    
      const handleSubmit = (values, actions) => {console.log(values)}
    
      return (
        <>
        <AppForm
          initialValues={initialValues}
          validationSchema={checkSchema}
          onSubmit={handleSubmit}
        >
          <AppFormInputField name='firstName' label='Enter First Name' />
          <AppFormFileUpload name='file' multiple/>
          <SubmitButton title='Submit' />    
        </AppForm>
        </>
      );
    };
    
    export default SignUp;
    

    Hope this solution might be helpful...In this way the forms can be reuse with their respective formik and yup validation