Molly Christian
Molly Christian

Reputation: 167

React Js Validation

Whats the best way to perform form validations in React. Can't we just use HTML5 validations such as a required field or an email field validations. For complex validation what the best way to do with React

Upvotes: 2

Views: 1039

Answers (2)

Brian Le
Brian Le

Reputation: 2686

An answer to your question

HTML 5 validation is too limited and can easily be manipulated. Hence client-side (using JS for better experience) and server-side validation (for security purposes)

My way of doing it

I believe this is the most annoying and time-consuming part of writing a React application so created myself a small react hook to simplify the job for me. It uses validator under the hood and returns event handlers as well as the field values. If you are planning to use this method, please read the code carefully to understand what's going on. Sounds simple but there are a few concepts you need to understand.

Let's start with plans

At first, I decided the shape of the input for the hook which looks like so:

fieldName: {
  value: '', // Initial state
    validationRules: [
      {
        method: funcToExec, // The validation function to execute
        validWhen: false,
        params: [param1, param2], // The parameters to pass down to the function
        errorText: 'This field should not be left empty!', // The error to prompt the user
      },
      {
        method: equals,
        compareTo: "anotherFieldName", // Used for cross field validation
        errorText: 'This field should have length between 6 and 20 characters!',
      },
    ],
  },

My validation strategy was as follow:

  • Do not show any errors when the user hasn't touched the input fields yet.
  • When the user stops tying for a certain amount of time, validate that field.
  • When the user clicks outside (blurs) the field, immediately validate the field.
  • The error text for a field should be cleared when the user focuses on that field.
  • All error text should be cleared when the user submits the form.
  • Validate the whole form again after it has been submitted. Show the error text one by one.

Debouncing is also essential as it improves the user interface.

Let's dive into some code

With these ideas being listed out, I started off by writing a transformer that transforms the input into the initial state for the useState() hook as follow:

const initialFieldState = {};

for (const key of Object.keys(fields))
 initialFieldState[key] = {
   value: fields[key].value,
   isValid: true, // Initial state has to be true as we assume that the user has touched the field yet
   errorText: null,
 };

After that, I wrote this small validation function:

const validate = useCallback((valueToValidate, rules) => {
  for (let i = 0; i < rules.length; i += 1) {
    const { method, params = [], validWhen = true, errorText } = rules[i];

    if (method(valueToValidate, ...params) !== validWhen) return [false, errorText];
  }
  return [true, null]; // Destructured as [isValid, errorText]
}, []);

My next step was to create 3 event handlers: onChange, onFocus, onBlur. But before doing that I realised I needed a debounced function that calls validate to perform validation and set the field states. So I made this, quite intuitive:

const validateField = useCallback(
  debounce((name, value, callback) => {
    const currentRules = fields[name].validationRules;
    const enhancedRules = enhanceValidationRules(currentRules);
    const [isValid, errorText] = validate(value, enhancedRules);

    setFieldState(prevState => ({
      ...prevState,
      [name]: {
        ...prevState[name],
        isValid,
        errorText,
      },
    }));

    if (callback != null) callback(isValid); // Don't worry about this callback, I'll explain later
  }, 2000),
  [],
);

Which calls enhanceValidationRules which basically just manipulates the validationRules slightly for cross field validation like so:

const enhanceValidationRules = useCallback(rules => {
  // Split the rules into 2 parts: The ones with cross field validations and the ones without
  const [crossFieldRules, otherRules] = partition(rules, ({ compareTo }) => !!compareTo);
  // Manually add values of other fields as params
  const enhancedCrossFieldRules = crossFieldRules.map(rule => ({
    ...rule,
    params: [latestFieldState.current[rule.compareTo].value],
  }));

  // Merge them again
  return [...otherRules, ...enhancedCrossFieldRules];
}, []);

Notice latestFieldState is a ref used to store the latest state of the fields as setState() is async in React. Further reference

After this, the event handlers are pieces of cake:

const handleFieldChange = useCallback(
  e => {
    const { name, value } = e.target;

    setFieldState(prevState => ({
      ...prevState,
      [name]: {
        ...prevState[name],
        value,
      },
    }));

    validateField(name, value);
  },
  [validateField],
);

const handleFieldBlur = useCallback(
  e => {
    const { name, value } = e.target;
    validateField(name, value);

    // Should validate immediately when user unfocuses the field
    validateField.flush();
  },
  [validateField],
);

const handleFieldFocus = useCallback(
  e => {
    const { name, value } = e.target;

    setFieldState(prevState => ({
      ...prevState,
      [name]: {
        ...prevState[name],
        errorText: null,
      },
    }));

    // Validate after the user focuses on the field
    validateField(name, value);
  },
  [validateField],
);

Obviously, you need to clear out the debounced function as well, like so:

useEffect(() => () => validateField.cancel(), [validateField]);

Okay, ALMOST THERE!

You might want to validate the whole form on more time after submission, but validateField doesn't return the value immediately. flush doesn't work either as it doesn't allow us to assign values to another variable. This is when callback comes into play. So we can implement it like so:

const validateFieldAfterSubmit = useCallback(
  (name, value) => {
    let pass;
    validateField(name, value, isValid => {
      pass = isValid;
    });

    validateField.flush();

    return pass;
  },
  [validateField],
);

That's it, you got it! The code is posted here as well as how you can use it. I used firebase and material-ui to make a full blown login box to demonstrate how useful and clean the hook could be.

Please feel free to ask me questions or report any bugs.

Upvotes: 2

James Delaney
James Delaney

Reputation: 1776

You can use HTML5 validations, here is article about that.

Here are two more resources for form validations in react:

Upvotes: 0

Related Questions