Form Validation in React.js

While working on a React project, I needed to implement robust form validation. Getting validation right is crucial for user experience and data integrity, but React doesn’t come with built-in validation tools.

In this comprehensive guide, I will walk you through multiple approaches to implement form validation in React.js, from simple client-side techniques to more advanced solutions.

Let’s get in!

Why Form Validation Matters in React Applications

Form validation ensures users submit accurate, properly formatted data. Without proper validation, your application risks collecting incorrect information or even becoming vulnerable to security issues.

React’s component-based architecture makes it ideal for creating reusable form validation patterns that can be implemented across your entire application.

Read How to Handle Events in React JS

Method 1: Built-in HTML5 Validation

The simplest way to add validation to React forms is by leveraging HTML5’s built-in validation attributes.

function SimpleForm() {
  const handleSubmit = (e) => {
    e.preventDefault();
    // Form processing logic here
    console.log("Form submitted successfully!");
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          required
          placeholder="Enter your email"
        />
      </div>

      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          required
          minLength="8"
          placeholder="Enter your password"
        />
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

You can see the output in the screenshot below.

HTML5 Validation Form Validation in React.js

In this example, the email field uses the type="email" attribute that automatically validates email format, while the password field requires a minimum length of 8 characters.

While this approach is simple, it offers limited customization for error messages and validation logic.

Method 2: Custom Form Validation with useState

For more control over validation, we can implement custom validation using React’s useState hook.

import { useState } from 'react';

function CustomValidationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    zipCode: ''
  });

  const [errors, setErrors] = useState({});

  const validateForm = () => {
    let tempErrors = {};

    if (!formData.username) {
      tempErrors.username = "Username is required";
    }

    if (!formData.email) {
      tempErrors.email = "Email is required";
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      tempErrors.email = "Email is invalid";
    }

    if (!formData.zipCode) {
      tempErrors.zipCode = "ZIP code is required";
    } else if (!/^\d{5}(-\d{4})?$/.test(formData.zipCode)) {
      tempErrors.zipCode = "Please enter a valid US ZIP code";
    }

    setErrors(tempErrors);
    return Object.keys(tempErrors).length === 0;
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validateForm()) {
      // Process form data
      console.log("Form submitted successfully!", formData);
      // Reset form
      setFormData({ username: '', email: '', zipCode: '' });
    } else {
      console.log("Form validation failed");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username:</label>
        <input
          type="text"
          id="username"
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
        {errors.username && <p className="error">{errors.username}</p>}
      </div>

      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="text"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <p className="error">{errors.email}</p>}
      </div>

      <div>
        <label htmlFor="zipCode">ZIP Code:</label>
        <input
          type="text"
          id="zipCode"
          name="zipCode"
          value={formData.zipCode}
          onChange={handleChange}
        />
        {errors.zipCode && <p className="error">{errors.zipCode}</p>}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

You can see the output in the screenshot below.

Custom Form Validation in React.js

This approach gives you complete control over validation logic and error messages. Notice how I’ve included a US-specific ZIP code validation pattern, which ensures users enter a valid 5-digit code or 5+4 format.

Method 3: Validation with Formik and Yup

For larger applications, using a dedicated form library like Formik paired with Yup for schema validation offers a more scalable solution.

First, install the required packages:

npm install formik yup

Then implement your form:

import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

function FormikValidationForm() {
  // Define validation schema
  const validationSchema = Yup.object({
    firstName: Yup.string()
      .max(15, 'Must be 15 characters or less')
      .required('Required'),
    lastName: Yup.string()
      .max(20, 'Must be 20 characters or less')
      .required('Required'),
    email: Yup.string()
      .email('Invalid email address')
      .required('Required'),
    ssn: Yup.string()
      .matches(/^\d{3}-\d{2}-\d{4}$/, 'Please enter a valid SSN (e.g., 123-45-6789)')
      .required('Required'),
  });

  return (
    <div>
      <h2>Registration Form</h2>
      <Formik
        initialValues={{
          firstName: '',
          lastName: '',
          email: '',
          ssn: ''
        }}
        validationSchema={validationSchema}
        onSubmit={(values, { setSubmitting, resetForm }) => {
          // Simulate form submission
          setTimeout(() => {
            alert(JSON.stringify(values, null, 2));
            setSubmitting(false);
            resetForm();
          }, 400);
        }}
      >
        {({ isSubmitting }) => (
          <Form>
            <div>
              <label htmlFor="firstName">First Name</label>
              <Field name="firstName" type="text" />
              <ErrorMessage name="firstName" component="div" className="error" />
            </div>

            <div>
              <label htmlFor="lastName">Last Name</label>
              <Field name="lastName" type="text" />
              <ErrorMessage name="lastName" component="div" className="error" />
            </div>

            <div>
              <label htmlFor="email">Email</label>
              <Field name="email" type="email" />
              <ErrorMessage name="email" component="div" className="error" />
            </div>

            <div>
              <label htmlFor="ssn">Social Security Number</label>
              <Field name="ssn" type="text" placeholder="123-45-6789" />
              <ErrorMessage name="ssn" component="div" className="error" />
            </div>

            <button type="submit" disabled={isSubmitting}>
              {isSubmitting ? 'Submitting...' : 'Submit'}
            </button>
          </Form>
        )}
      </Formik>
    </div>
  );
}

You can see the output in the screenshot below.

Formik and Yup Form Validation in React.js

Formik handles form state, submission, and validation while Yup provides a simple way to define validation schemas. In this example, I’ve included validation for a US Social Security Number, showing how you can implement country-specific validation patterns.

Method 4: Real-time Validation with useEffect

Sometimes, you want to validate fields as users type, rather than waiting for form submission:

import { useState, useEffect } from 'react';

function RealtimeValidationForm() {
  const [creditCard, setCreditCard] = useState('');
  const [error, setError] = useState('');

  // Validate credit card as user types
  useEffect(() => {
    if (!creditCard) {
      setError('');
      return;
    }

    // Remove spaces and dashes
    const cleaned = creditCard.replace(/\s+/g, '').replace(/-/g, '');

    // Check if input contains only digits
    if (!/^\d+$/.test(cleaned)) {
      setError('Credit card should contain only numbers, spaces, or dashes');
      return;
    }

    // Validate card number using Luhn algorithm
    if (cleaned.length >= 13) {
      // Simple length check for demonstration
      if (cleaned.length < 13 || cleaned.length > 19) {
        setError('Credit card number is invalid');
      } else {
        setError('');
      }
    }
  }, [creditCard]);

  const handleChange = (e) => {
    // Format as user types: Add space after every 4 digits
    const input = e.target.value;
    const formatted = input.replace(/[^\d]/g, '').replace(/(.{4})/g, '$1 ').trim();
    setCreditCard(formatted);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!error && creditCard) {
      alert('Valid credit card submitted!');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="creditCard">Credit Card Number:</label>
        <input
          type="text"
          id="creditCard"
          value={creditCard}
          onChange={handleChange}
          placeholder="1234 5678 9012 3456"
          maxLength="19"
        />
        {error && <p className="error">{error}</p>}
      </div>

      <button type="submit" disabled={!!error || !creditCard}>
        Submit
      </button>
    </form>
  );
}

This example demonstrates real-time credit card validation and formatting. As the user types, spaces are automatically inserted for improved readability, and validation occurs with each keystroke.

Method 5: Use React Hook Form

React Hook Form is a lightweight library that offers excellent performance with minimal re-renders:

  import { useForm } from 'react-hook-form';

function ReactHookForm() {
  const { 
    register, 
    handleSubmit, 
    formState: { errors },
    watch
  } = useForm();
  
  const password = watch("password");
  
  const onSubmit = (data) => {
    console.log(data);
    alert("Form submitted successfully!");
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          {...register("email", {
            required: "Email is required",
            pattern: {
              value: /\S+@\S+\.\S+/,
              message: "Please enter a valid email"
            }
          })}
        />
        {errors.email && <p className="error">{errors.email.message}</p>}
      </div>
      
      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password" {...register("password", { required: "Password is required" })} />
           <div>
    <label htmlFor="confirmPassword">Confirm Password:</label>
    <input
      type="password"
      id="confirmPassword"
      {...register("confirmPassword", {
        required: "Please confirm your password",
        validate: value => value === password || "Passwords do not match"
      })}
    />
    {errors.confirmPassword && <p className="error">{errors.confirmPassword.message}</p>}
  </div>
  
  <div>
    <label htmlFor="phone">Phone Number:</label>
    <input
      id="phone"
      {...register("phone", {
        required: "Phone number is required",
        pattern: {
          value: /^\(\d{3}\) \d{3}-\d{4}$/,
          message: "Please enter a valid US phone number format: (123) 456-7890"
        }
      })}
      placeholder="(123) 456-7890"
    />
    {errors.phone && <p className="error">{errors.phone.message}</p>}
  </div>
  
  <button type="submit">Register</button>
</form>
);
}

React Hook Form reduces the amount of code needed while providing excellent validation capabilities. This example includes complex password validation with requirements for special characters, numbers, and mixed case, a common security requirement for modern web applications.

Method 6: Combine Server and Client-Side Validation

For critical applications, it’s best to validate both on the client and server side:

```jsx
import { useState } from 'react';

function DualValidationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    taxId: ''
  });
  
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [serverErrors, setServerErrors] = useState(null);
  
  // Client-side validation
  const validateForm = () => {
    let tempErrors = {};
    
    if (!formData.username) {
      tempErrors.username = "Username is required";
    } else if (formData.username.length < 3) {
      tempErrors.username = "Username must be at least 3 characters";
    }
    
    if (!formData.email) {
      tempErrors.email = "Email is required";
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      tempErrors.email = "Email is invalid";
    }
    
    // US Tax ID (SSN) validation
    if (!formData.taxId) {
      tempErrors.taxId = "Tax ID is required";
    } else if (!/^\d{3}-\d{2}-\d{4}$/.test(formData.taxId)) {
      tempErrors.taxId = "Please enter a valid SSN (e.g., 123-45-6789)";
    }
    
    setErrors(tempErrors);
    return Object.keys(tempErrors).length === 0;
  };
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // First run client-side validation
    if (validateForm()) {
      setIsSubmitting(true);
      setServerErrors(null);
      
      try {
        // Mock API call - in a real app, this would be your API endpoint
        const response = await mockServerValidation(formData);
        
        if (response.success) {
          alert("Form submitted successfully!");
          setFormData({ username: '', email: '', taxId: '' });
        } else {
          // Handle server validation errors
          setServerErrors(response.errors);
        }
      } catch (error) {
        setServerErrors({ general: "An error occurred. Please try again." });
      } finally {
        setIsSubmitting(false);
      }
    }
  };
  
  // This simulates a server validation call
  const mockServerValidation = (data) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        // Simulate server validation
        const serverErrors = {};
        
        // Simulate checking if email already exists in database
        if (data.email === "[email protected]") {
          serverErrors.email = "This email is already registered";
        }
        
        if (Object.keys(serverErrors).length > 0) {
          resolve({ success: false, errors: serverErrors });
        } else {
          resolve({ success: true });
        }
      }, 1000);
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {serverErrors && serverErrors.general && (
        <div className="server-error">{serverErrors.general}</div>
      )}
      
      <div>
        <label htmlFor="username">Username:</label>
        <input
          type="text"
          id="username"
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
        {errors.username && <p className="error">{errors.username}</p>}
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="text"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <p className="error">{errors.email}</p>}
        {serverErrors && serverErrors.email && (
          <p className="server-error">{serverErrors.email}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="taxId">Social Security Number:</label>
        <input
          type="text"
          id="taxId"
          name="taxId"
          value={formData.taxId}
          onChange={handleChange}
          placeholder="123-45-6789"
        />
        {errors.taxId && <p className="error">{errors.taxId}</p>}
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Submitting..." : "Submit"}
      </button>
    </form>
  );
}

This method provides the most comprehensive validation by checking data on both the client and server sides. It prevents invalid data submission and catches issues that can only be verified on the server, like duplicate email addresses in your database.

Work with React Date Picker Component

Best Practices for Form Validation in React

After implementing form validation across numerous React applications, I’ve found these best practices invaluable:

  1. Provide immediate feedback: Validate fields as users type or when they leave a field, not just on submission.
  2. Use clear error messages: Explain exactly what’s wrong and how to fix it.
  3. Highlight problematic fields: Use visual cues like red borders or icons to draw attention to fields with errors.
  4. Disable submission when invalid: Prevent users from submitting forms with validation errors.
  5. Consider accessibility: Ensure error messages are properly associated with their fields using aria attributes.
  6. Balance security and usability: Implement strict validation where security matters (passwords, financial data), but keep it simpler for less critical fields.

Form validation is an essential aspect of creating robust, user-friendly React applications. By implementing these techniques, you’ll ensure your applications collect accurate data while providing a smooth user experience.

Whether you choose built-in HTML validation, a custom approach with hooks, or a specialized library like Formik or React Hook Form, the key is consistency across your application. This creates a predictable experience that users will appreciate.

Related tutorials:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.