DEV Community

lineldcosta
lineldcosta

Posted on

Custom React hook form validation with yup & material ui

Doing any freelancing? want to finish the task quickly? Just follow below, apply on any forms and finsh form validation quickly.

Create a reducer

const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_VALUE':
      return {
        ...state,
        [FORMDATA]: action.payLoad,
      };
    case 'SET_ERROR':
      return {
        ...state,
        [ERRORS]: action.payLoad,
      };
    case 'ON_CHANGE':
      return {
        ...state,
        ...action.payLoad,
      };
    default:
      return state;
  }
};

Use the below custom hook


export const useForm = (schema, defaultFormValues = {}) => {
  const [state, dispatch] = useReducer(reducer, { [FORMDATA]: defaultFormValues, [ERRORS]: {} });
  const { formData, errors } = state;

  // Commit state result
  const dispatchValue = (field, value, result) => {
    if (result instanceof Yup.ValidationError) {
      // Error
      errors[field] = result.message;
    } else {
      // Delete current state result
      delete errors[field];
    }

    // Update state, for object update, need a clone
    const newState = { [ERRORS]: errors, [FORMDATA]: { ...formData, [field]: value } };
    dispatch({ type: 'ON_CHANGE', payLoad: newState });
  };

  const updateFormValue = (field, value) => {
    // Validate the field, then before catch, if catch before then, both will be triggered

    Yup.reach(schema(), field)
      .validate(value)
      .then((result) => {
        dispatchValue(field, value, result);
      })
      .catch((result) => {
        dispatchValue(field, value, result);
      });
  };

  return {
    validate: async () => {
      const validationResult = { isValid: true, errors: {} };
      try {
        await schema().validate(formData, {
          strict: true,
          abortEarly: false,
          stripUnknown: false,
        });
      } catch (e) {
        const formValidationError = {};
        if (e instanceof Yup.ValidationError) {
          // eslint-disable-next-line no-restricted-syntax
          for (const error of e.inner) {
            // Only show the first error of the field
            if (!formValidationError[error.path]) {
              formValidationError[error.path] = error.message;
            }
          }
        }
        dispatch({ type: 'SET_ERROR', payLoad: formValidationError });
        validationResult.isValid = false;
        validationResult.errors = formValidationError;
        return validationResult;
      }

      return validationResult;
    },
    setValue: (formInput) => {
      //  In case where we need to set all the form values
      if (Array.isArray(formInput)) {
        const [defaultObj] = formInput;
        dispatch({ type: 'SET_VALUE', payLoad: { ...formData, ...defaultObj } });
      } else {
        const { name, value } = formInput;
        updateFormValue(name, value);
      }
    },
    errors: (field) => errors[field],
    getValues: () => formData,
    value: (field) => state[field],
    onFormBlur: (name, value) => {
      updateFormValue(name, value);
    },
  };
};

To handle form value and validation just import the above hook.


  const { validate, setValue, errors, getValues } = useForm(validateSchema, formData);

You can use yup to define the schema

import * as yup from 'yup';

// eslint-disable-next-line import/prefer-default-export
export const validateSchema = () =>
  yup.object().shape({
    service: yup
      .number()
      .required('Please provide the service')
  });


To get the form values you can use the method exported from useFrom hook like below.

  const initialState = getValues();

If you are using material-ui then use like below.

  <TextField
    id={ROLE}
    name={ROLE}
    label="Role"
    placeholder=""
    maxLength="15"
    fullWidth
    required
    margin="dense"
    value={initialState[ROLE]}
    autoComplete="off"
    disabled={isLoading || isSubmiting || !initialState[SERVICE]}
    onChange={onChangeInput(ROLE)}
    error={!!errors(ROLE)}
    helperText={errors(ROLE)}
    InputLabelProps={{
      shrink: true,
      'aria-label': 'Select Role',
    }}
  />

If you have any suggetions add comments.... happy coding!!

Top comments (0)