Form Validation

Difficulty: Intermediate

Form validation ensures user input meets expected criteria before submission. In React, client-side validation is typically implemented with controlled components, where the state contains both the form values and any validation errors. Validation can occur on change (as the user types), on blur (when the user leaves a field), or on submit (when the form is submitted). Each timing has different user experience implications.

Validation on change provides immediate feedback but can be annoying for incomplete input (showing 'invalid email' before the user finishes typing). Validation on blur is a good middle ground - it waits until the user moves to the next field before checking. Validation on submit is the simplest but provides the latest feedback, requiring the user to submit before seeing any errors. Many forms combine these: validate on blur for individual fields and validate all fields on submit.

The typical validation pattern uses an errors object alongside the form values in state. Each key in the errors object corresponds to a form field, and its value is the error message (or an empty string / undefined for valid fields). A field is considered invalid when its error value is truthy. The submit handler checks all fields and either proceeds with submission or updates the errors state.

Validation functions should be pure and reusable. A common pattern is to create a validate function that takes the entire form state and returns an errors object. This makes validation logic easy to test in isolation and reuse across similar forms. For complex validation (interdependent fields, async validation like checking username availability), the validate function can return promises.

Preventing submission while the form is invalid is straightforward: check the errors object before proceeding. You can disable the submit button when errors exist, show a summary of all errors at the top of the form, or highlight individual fields with their specific error messages. Good form validation provides clear, specific error messages and highlights the problematic fields.

Code examples

Basic form validation

function validateForm(values) {
  const errors = {};

  if (!values.name) {
    errors.name = 'Name is required';
  } else if (values.name.length < 2) {
    errors.name = 'Name must be at least 2 characters';
  }

  if (!values.email) {
    errors.email = 'Email is required';
  } else if (!values.email.includes('@')) {
    errors.email = 'Email must contain @';
  }

  return errors;
}

// Test valid input
let errors = validateForm({ name: 'Alice', email: 'alice@test.com' });
console.log('Valid form errors:', JSON.stringify(errors));
console.log('Is valid:', Object.keys(errors).length === 0);

// Test invalid input
errors = validateForm({ name: '', email: 'invalid' });
console.log('Invalid form errors:', JSON.stringify(errors));
console.log('Is valid:', Object.keys(errors).length === 0);

The validate function takes form values and returns an errors object. An empty errors object means the form is valid. Each key maps to a field name and the value is the error message.

Validation on submit

function LoginForm() {
  const [values, setValues] = React.useState({ email: '', password: '' });
  const [errors, setErrors] = React.useState({});

  const validate = (vals) => {
    const errs = {};
    if (!vals.email) errs.email = 'Email required';
    if (!vals.password) errs.password = 'Password required';
    else if (vals.password.length < 6) errs.password = 'Min 6 characters';
    return errs;
  };

  const handleSubmit = (formValues) => {
    const validationErrors = validate(formValues);
    setErrors(validationErrors);

    if (Object.keys(validationErrors).length === 0) {
      console.log('Form submitted successfully');
    } else {
      console.log('Validation failed:', JSON.stringify(validationErrors));
    }
  };

  // Test with invalid data
  console.log('Submitting empty form...');
  handleSubmit({ email: '', password: '123' });

  // Test with valid data
  console.log('Submitting valid form...');
  handleSubmit({ email: 'user@test.com', password: 'secure123' });

  return null;
}

LoginForm();

The submit handler validates all fields before proceeding. If errors exist, they are stored in state and displayed to the user. Only when all fields are valid does the submission proceed.

Field-level validation on blur

function validateField(name, value) {
  switch (name) {
    case 'username':
      if (!value) return 'Username is required';
      if (value.length < 3) return 'Username must be at least 3 characters';
      return '';
    case 'age':
      if (!value) return 'Age is required';
      if (isNaN(Number(value))) return 'Age must be a number';
      if (Number(value) < 18) return 'Must be at least 18';
      return '';
    default:
      return '';
  }
}

// Simulate blur events
console.log('Blur username empty:', validateField('username', ''));
console.log('Blur username short:', validateField('username', 'ab'));
console.log('Blur username valid:', validateField('username', 'alice'));
console.log('Blur age invalid:', validateField('age', 'abc'));
console.log('Blur age young:', validateField('age', '15'));
console.log('Blur age valid:', validateField('age', '25'));

Field-level validation runs when the user leaves a field (blur event). Each field has its own validation rules. An empty string return means the field is valid.

Key points

Concepts covered

client-side validation, error states, form submission, validation patterns, error messages