Search
  • Sotiris Karapostolou

Managing Forms in React-Native. Build your own Formik with Validation using Hooks, Context API & Yup



As a frontend developer I've used and implemented many libraries, but one of my favorite was Formik that's why I've decided to build it myself and I must say it was very plesant.


I was working on the Setting page the other day, As you see in the gif above it's just a screen where the user can view or update his info.


We’re Building

There are couple of things happening in this form:

  1. The form holds initial values that can be changed and get submited with an onSubmit() callback function

  2. It validates user input with a provided validationShema using Yup

  3. Throws erros based on the validation shema

  4. Sets focus on the next input by using a registeredField() function behind the scenes that wires up refs

  5. Reduces a lot of boilerplate and gives us easy access to form's state (values, errors, touched, focused)

  6. Provides us a custom textInput, submit button and label components with our own defined styles for our accross platform unique styling that handles all the functionality behind the scens and updates the UI whenever a user interacts with it

And many more. The most fun part is that we just use React and pure functions.

Once we set it up it's just a few lines of code that gets the job done!!


Now let's look have a look at the Settings.js component:


#Settings.js

import * as Yup from 'yup';
import { CustomFormik, FormButton, Field } from 'services/CustomFormik';

const UserSettings = ({ navigation }) => {
 const profileData = navigation.getParam('profileData');
 const dispatch = useDispatch();

 const initialValues = {
   name: profileData.name ?? '',
   email: profileData.email ?? '',
   ....
  };

 const validationShema = Yup.object().shape({
   name: Yup.string().required('Required'),
   email: Yup.string().email('"Invalid email address'),
   ....
  });

 const settingInputs = Object.keys(initialValues);

 return (
   <CustomFormik
     initialValues={initialValues}
     validationShema={validationShema}
     onSumbit={({ values }) => {
       dispatch(updateUserProfileStart(values));
     }}
   >
    <ScrollView>
      {settingInputs.map((input, i) => (
         <Field
           name={input}
           key={i}
           label={input}
           containerStyles={{ marginVertical: 10 }}
           placeholderTextColor={Colors.INPUT_TEXT}
           focusNext={settingInputs[i + 1]}
         />
      ))}
      <FormButton
        loading={loading}
        title="Login"
        color={Colors.SECONDARY}
        textColor={Colors.WHITE}
      />
    </ScrollView>
   </CustomFormik>
  );
};

Easy right ??

Setup

To get started I’ve put together a repository on github to view the full code

First let's install this package so we can use it later

$ npm install yup

The basic structure of the CustomFormik.js file looks like:


#CustomFormik.js

import React, { useState, useRef, createContext } from 'react';
const CustomForm = createContext({});

export const CustomFormik = (props) => {
 const [formData, setFormData] = useState({...});
 const fieldRegistry = useRef({});
 ...
 return (
    <CustomForm.Provider value={...}>
     ...
    </CustomForm.Provider>
  );
};

export const Field = (props) => (
 <CustomForm.Consumer>
  {(props) => {
    return (
      ...
    );
  }}
 </CustomForm.Consumer>
);
 
export const FormButton = (props) =>  (
 <CustomForm.Consumer>
   {(props) => (
     ....
   )}
 </CustomForm.Consumer>
); 
 

So far our form doesn't do anything, we just created the context so our components can share values between each other


Next we will take a closer look to each component. First, we start with CustomFormik



export const CustomFormik = (props) => {

 const [formData, setFormData] = useState({
    values: props.initialValues || {},
    touched: {},
    error: {},
    focused: undefined,
    isSubmiting: false,
  });

 const fieldRegistry = useRef({});

// This way we keep track of the input's refs by registering them to the fieldRegistry.

 const registeredField = (name) => (ref) => {
   if (fieldRegistry.current) {
      fieldRegistry.current[name] = { ref };
   }
  };
// This is the input change handler, it takes user's input and updates the state with the new value.

 const handleChange = (name) => (text) => {
   setFormData((prev) => ({
      ...prev,
      values: {
        ...prev.values,
        [name]: text,
      },
    }));
  };
// This is the onBlur event handler, when the user removes focus from an input, this function is called. Without it, if there are any errors in the input when it loses focus, the errors will only display when the user tries to submit.

 const handleBlur = (name) => (text) => {
   setFormData((prev) => ({
      ...prev,
       touched: {
        ...prev.touched,
        [name]: true,
       },
    }));
  };
// This is the onFocus event handler, that watches if a form field has focus,it updates the state and sets focus to the field.

 const setFocus = (name) => () => {
   if (fieldRegistry.current) {
     setFormData((prev) => ({
        ...prev,
       focused: name,
      }));
     if (name) fieldRegistry.current[name].ref.focus();
   }
  };
// Keep tracks of the validation errors. 

 const setErros = (name, message) => {
   setFormData((prev) => ({
      ...prev,
       error: {
        ...prev.error,
        [name]: message,
      },
    }));
  };
// Validates a deeply nested path within the schema.

 async function onValidateAt(name) {
   try {
     await props.validationShema.validateAt(name, formData.values).then(() => {
     setFormData((prev) => ({
          ...prev,
          error: {
            ...prev.error,
            [name]: undefined,
          },
        }));
      });
    } catch (err) {
     setErros(err.path, err.message);
    }
  }
// Validates all the values and updates the error Obj if needed.

 async function onValidate() {
   try {
     await props.validationShema
        .validate(formData.values, { abortEarly: false })
        .then(() => {
          setFormData((prev) => ({
            ...prev,
            error: {},
          }));
        });
    } catch (err) {
       err.inner.forEach((e) => {
         setErros(e.path, e.message);
      });
    }
  }
// The form submission handler checks the validationShema.isValid for errors and then passes important information to the provided callback.

 async function handleSubmit() {
   if (props.validationShema) {
     onValidate();
     await props.validationShema
        .isValid(formData.values)
        .then(function (valid) {
          if (valid) props.onSumbit({ ...formData });
         });
    } else {
     props.onSumbit({ ...formData });
    }
  }
// In case it's children needs access to the state.

function checkChildren() {
    if (typeof props.children === 'object') {
       return props.children;
    } else {
       return props.children({ ...formData });
    }
  }
// We update the value object of the provider component to allow consuming components to subscribe to context changes.

  return (
     <FormikData.Provider
        value={{
          ...formData,
          handleChange,
          handleBlur,
          handleSubmit,
          onValidateAt,
          registeredField,
          setFocus,
        }}
     >  
       {checkChildren()}
     </FormikData.Provider>
   );


};  // End of CustomFormik component

We covered a lot of functionality needed to get our form working but once you set it up it's a matter of few line (see Settings.js at the top of the article) to build a new form whenever your application needs it.


Next let's make a few changes to the Field component to listen to the functions we've just implemented.



export const Field = ({ 
 name,containerStyles,focusNext,label, ...otherProps
}) =>  (
<FormikData.Consumer>
 {({
 handleChange,handleBlur,onValidateAt,registeredField,values,
 error,setFocus,focused }) => {
 return (
   <View style={containerStyles}>
     {label && (
       <Text>{label}</Text>
     )}
     <TextInput
       value={values[name]}
       ref={registeredField(name)}
       onSubmitEditing={setFocus(focusNext)}
       onChangeText={handleChange(name)}
       onBlur={handleBlur(name)}
       onFocus={setFocus(name)}
       onEndEditing={onValidateAt.bind(this, name)}
       returnKeyType={focusNext ? 'next' : 'done'}
       style={{
       borderColor: focused === name ? '#anyColor' : '#anyColor',
       }}
       {...otherProps}
     />
     {error[name] && (
       <Text style={{ color: Colors.ERROR}}>
         {error[name]}
       </Text>
     )}
   </View>
  );}}
</FormikData.Consumer>
);

Finally to be able to submit the form let's tweak the FormButton component



export const FormButton = (props) => {
 return (
   <FormikData.Consumer>
     {({ handleSubmit }) => (
      <>
       {props.loading ? (
         <ActivityIndicator color={Colors.PRIMARY} />
       ) : (
         <Button
           title={props.title}
           color={props.color}
           onPress={handleSubmit}
         />
       )}
      </>
     )}
   </FormikData.Consumer>
  );
};

It goes without saying that you can use any button you prefer, In my case for the UserSettings component I am using a React Native Floating Action button that I imported from this library:

$ npm i react-native-floating-action

To Sum Up

Building your own components is such a great exercise to just sit down, spend the afternoon, build it out, tweak it, try to optimize that, and all that stuff. It's a fun, great way to play with it, I think.


Well, I hope that helped! Right to the point! If you like this and want to support me on making more stories you can contact me on Linkedin


#react#reactNative#hooks#contextAPI#yup#formik#forms#buildItYourSelf