Dung (Donny) Nguyen

Senior Software Engineer

Forms & Validation

Forms are one of the most common ways users interact with a web application—logging in, signing up, searching, checkout, and more. In React, forms require a bit of extra thought because the UI is driven by state. This article covers how to build forms, capture user input, validate that input, and manage complex forms with popular libraries like React Hook Form, Formik, and Yup.

Controlled vs. Uncontrolled Forms

React offers two ways to handle form inputs:

Controlled components are the recommended default because they make validation, conditional logic, and dynamic UI easier.

Building a Controlled Form

import { useState } from 'react';

function LoginForm() {
  const [form, setForm] = useState({ email: '', password: '' });

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Submitting:', form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        value={form.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
        placeholder="Password"
      />
      <button type="submit">Log In</button>
    </form>
  );
}

export default LoginForm;

Manual Validation

For simple forms, you can validate in the submit handler and store error messages in state.

import { useState } from 'react';

function SignupForm() {
  const [form, setForm] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const newErrors = {};
    if (!form.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(form.email)) {
      newErrors.email = 'Email is invalid';
    }
    if (form.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }
    return newErrors;
  };

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

  const handleSubmit = (e) => {
    e.preventDefault();
    const validationErrors = validate();
    setErrors(validationErrors);
    if (Object.keys(validationErrors).length === 0) {
      console.log('Valid form:', form);
    }
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <input
          name="email"
          value={form.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      <div>
        <input
          name="password"
          type="password"
          value={form.password}
          onChange={handleChange}
          placeholder="Password"
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      <button type="submit">Sign Up</button>
    </form>
  );
}

export default SignupForm;

Manual validation works, but it becomes repetitive and error-prone as forms grow. This is where dedicated libraries shine.

React Hook Form

React Hook Form is a lightweight, performant library that uses uncontrolled inputs under the hood (via refs) to minimize re-renders. It is the most popular form library in the modern React ecosystem.

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

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = (data) => {
    console.log('Valid form:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        placeholder="Email"
        {...register('email', {
          required: 'Email is required',
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: 'Email is invalid',
          },
        })}
      />
      {errors.email && <span className="error">{errors.email.message}</span>}

      <input
        type="password"
        placeholder="Password"
        {...register('password', {
          required: 'Password is required',
          minLength: { value: 8, message: 'At least 8 characters' },
        })}
      />
      {errors.password && <span className="error">{errors.password.message}</span>}

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

export default SignupForm;

Key benefits:

Formik

Formik is another widely used library that keeps form state in React state (controlled). It provides helpers for values, errors, touched fields, and submission.

npm install formik
import { useFormik } from 'formik';

function SignupForm() {
  const formik = useFormik({
    initialValues: { email: '', password: '' },
    validate: (values) => {
      const errors = {};
      if (!values.email) errors.email = 'Email is required';
      if (values.password.length < 8) errors.password = 'At least 8 characters';
      return errors;
    },
    onSubmit: (values) => {
      console.log('Valid form:', values);
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <input
        name="email"
        onChange={formik.handleChange}
        value={formik.values.email}
        placeholder="Email"
      />
      {formik.errors.email && <span className="error">{formik.errors.email}</span>}

      <input
        name="password"
        type="password"
        onChange={formik.handleChange}
        value={formik.values.password}
        placeholder="Password"
      />
      {formik.errors.password && <span className="error">{formik.errors.password}</span>}

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

export default SignupForm;

Schema Validation with Yup

Writing validation logic by hand gets messy. Yup lets you define a validation schema declaratively and reuse it across forms. It integrates cleanly with both React Hook Form and Formik.

npm install yup @hookform/resolvers
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required('Email is required').email('Email is invalid'),
  password: yup
    .string()
    .required('Password is required')
    .min(8, 'At least 8 characters'),
});

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({ resolver: yupResolver(schema) });

  const onSubmit = (data) => console.log('Valid form:', data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input placeholder="Email" {...register('email')} />
      {errors.email && <span className="error">{errors.email.message}</span>}

      <input type="password" placeholder="Password" {...register('password')} />
      {errors.password && <span className="error">{errors.password.message}</span>}

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

export default SignupForm;

A single schema keeps validation rules in one place, making forms easier to read, test, and maintain.

Best Practices

Conclusion

Forms in React revolve around managing input state and validating that input reliably. Start with controlled components and manual validation to understand the fundamentals, then reach for React Hook Form or Formik—paired with a schema library like Yup—to build robust, maintainable forms with minimal boilerplate.