reactjs
/

React Forms – Handling Form Inputs & Validation

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

reactjs

React Forms – Handling Form Inputs & Validation

Forms in React

React forms handle user input through component state. There are two main approaches: controlled components (React state controls input value) and uncontrolled components (DOM handles input state with refs).

Controlled Components

React JSXRead-only
1
import { useState } from 'react';

function ControlledForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    age: '',
    gender: '',
    interests: [],
    bio: ''
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    if (type === 'checkbox') {
      // Handle checkboxes
      if (checked) {
        setFormData({
          ...formData,
          interests: [...formData.interests, value]
        });
      } else {
        setFormData({
          ...formData,
          interests: formData.interests.filter(item => item !== value)
        });
      }
    } else {
      // Handle text inputs
      setFormData({
        ...formData,
        [name]: value
      });
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form submitted:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="username"
        value={formData.username}
        onChange={handleChange}
        placeholder="Username"
      />
      
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      
      <input
        type="number"
        name="age"
        value={formData.age}
        onChange={handleChange}
        placeholder="Age"
      />
      
      <select name="gender" value={formData.gender} onChange={handleChange}>
        <option value="">Select Gender</option>
        <option value="male">Male</option>
        <option value="female">Female</option>
        <option value="other">Other</option>
      </select>
      
      <label>
        <input
          type="checkbox"
          value="coding"
          checked={formData.interests.includes('coding')}
          onChange={handleChange}
        />
        Coding
      </label>
      
      <textarea
        name="bio"
        value={formData.bio}
        onChange={handleChange}
        placeholder="Tell us about yourself"
      />
      
      <button type="submit">Submit</button>
    </form>
  );
}

Uncontrolled Components

React JSXRead-only
1
import { useRef } from 'react';

function UncontrolledForm() {
  const usernameRef = useRef(null);
  const emailRef = useRef(null);
  const genderRef = useRef(null);
  const bioRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = {
      username: usernameRef.current.value,
      email: emailRef.current.value,
      gender: genderRef.current.value,
      bio: bioRef.current.value
    };
    console.log('Form data:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={usernameRef} defaultValue="" placeholder="Username" />
      <input type="email" ref={emailRef} defaultValue="" placeholder="Email" />
      <select ref={genderRef} defaultValue="">
        <option value="">Select Gender</option>
        <option value="male">Male</option>
        <option value="female">Female</option>
      </select>
      <textarea ref={bioRef} defaultValue="" placeholder="Bio" />
      <button type="submit">Submit</button>
    </form>
  );
}

// Using defaultValue for initial values
function DefaultValueForm() {
  return (
    <form>
      <input type="text" defaultValue="Initial value" />
      <input type="checkbox" defaultChecked={true} />
      <select defaultValue="male">
        <option value="male">Male</option>
        <option value="female">Female</option>
      </select>
    </form>
  );
}

Form Validation

React JSXRead-only
1
import { useState } from 'react';

function ValidatedForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const 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 'email':
        if (!value) return 'Email is required';
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(value)) return 'Invalid email format';
        return '';
      
      case 'password':
        if (!value) return 'Password is required';
        if (value.length < 6) return 'Password must be at least 6 characters';
        if (!/[A-Z]/.test(value)) return 'Password must contain at least one uppercase letter';
        if (!/[0-9]/.test(value)) return 'Password must contain at least one number';
        return '';
      
      case 'confirmPassword':
        if (!value) return 'Please confirm your password';
        if (value !== formData.password) return 'Passwords do not match';
        return '';
      
      default:
        return '';
    }
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
    
    // Validate on change if field has been touched
    if (touched[name]) {
      const error = validateField(name, value);
      setErrors({ ...errors, [name]: error });
    }
  };

  const handleBlur = (e) => {
    const { name, value } = e.target;
    setTouched({ ...touched, [name]: true });
    const error = validateField(name, value);
    setErrors({ ...errors, [name]: error });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Validate all fields
    const newErrors = {};
    let isValid = true;
    
    Object.keys(formData).forEach(field => {
      const error = validateField(field, formData[field]);
      if (error) {
        newErrors[field] = error;
        isValid = false;
      }
    });
    
    setErrors(newErrors);
    
    if (isValid) {
      console.log('Form submitted:', formData);
      alert('Form submitted successfully!');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Username:</label>
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.username && touched.username ? 'error' : ''}
        />
        {touched.username && errors.username && (
          <span style={{ color: 'red' }}>{errors.username}</span>
        )}
      </div>

      <div>
        <label>Email:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.email && touched.email ? 'error' : ''}
        />
        {touched.email && errors.email && (
          <span style={{ color: 'red' }}>{errors.email}</span>
        )}
      </div>

      <div>
        <label>Password:</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.password && touched.password ? 'error' : ''}
        />
        {touched.password && errors.password && (
          <span style={{ color: 'red' }}>{errors.password}</span>
        )}
      </div>

      <div>
        <label>Confirm Password:</label>
        <input
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.confirmPassword && touched.confirmPassword ? 'error' : ''}
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <span style={{ color: 'red' }}>{errors.confirmPassword}</span>
        )}
      </div>

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

Custom Form Hook

React JSXRead-only
1
import { useState, useCallback } from 'react';

function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = useCallback((e) => {
    const { name, value, type, checked } = e.target;
    const newValue = type === 'checkbox' ? checked : value;
    
    setValues(prev => ({ ...prev, [name]: newValue }));
    
    if (touched[name] && validate) {
      const error = validate(name, newValue, values);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  }, [touched, values, validate]);

  const handleBlur = useCallback((e) => {
    const { name, value } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
    
    if (validate) {
      const error = validate(name, value, values);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  }, [values, validate]);

  const handleSubmit = useCallback((onSubmit) => async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    // Validate all fields
    if (validate) {
      const newErrors = {};
      let isValid = true;
      
      Object.keys(values).forEach(field => {
        const error = validate(field, values[field], values);
        if (error) {
          newErrors[field] = error;
          isValid = false;
        }
      });
      
      setErrors(newErrors);
      
      if (isValid) {
        await onSubmit(values);
      }
    } else {
      await onSubmit(values);
    }
    
    setIsSubmitting(false);
  }, [values, validate]);

  const resetForm = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    resetForm,
    setValues
  };
}

// Usage
function LoginForm() {
  const validate = (name, value, values) => {
    if (name === 'email') {
      if (!value) return 'Email is required';
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email';
    }
    if (name === 'password') {
      if (!value) return 'Password is required';
      if (value.length < 6) return 'Password must be at least 6 characters';
    }
    return '';
  };

  const {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit
  } = useForm({ email: '', password: '' }, validate);

  const onSubmit = async (data) => {
    console.log('Login data:', data);
    // API call here
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          name="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Email"
        />
        {touched.email && errors.email && <span>{errors.email}</span>}
      </div>
      <div>
        <input
          name="password"
          type="password"
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Password"
        />
        {touched.password && errors.password && <span>{errors.password}</span>}
      </div>
      <button type="submit">Login</button>
    </form>
  );
}

React Hook Form (Recommended Library)

React JSXRead-only
1
import { useForm } from 'react-hook-form';

function ReactHookFormExample() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset
  } = useForm({
    defaultValues: {
      username: '',
      email: '',
      age: 18
    }
  });

  const onSubmit = async (data) => {
    console.log(data);
    await new Promise(resolve => setTimeout(resolve, 1000));
    reset();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          {...register('username', {
            required: 'Username is required',
            minLength: {
              value: 3,
              message: 'Username must be at least 3 characters'
            }
          })}
          placeholder="Username"
        />
        {errors.username && (
          <span>{errors.username.message}</span>
        )}
      </div>

      <div>
        <input
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: 'Invalid email format'
            }
          })}
          placeholder="Email"
        />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <input
          type="number"
          {...register('age', {
            min: { value: 18, message: 'Must be at least 18' },
            max: { value: 100, message: 'Must be at most 100' }
          })}
          placeholder="Age"
        />
        {errors.age && <span>{errors.age.message}</span>}
      </div>

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

Form Best Practices

  • Use controlled components for most forms (easier validation and state management)
  • Implement validation on both client and server side
  • Show validation errors inline, after field blur or on submit
  • Disable submit button during submission to prevent duplicates
  • Provide clear error messages that explain how to fix the issue
  • Use form libraries like React Hook Form or Formik for complex forms
  • Handle accessibility with proper labels and ARIA attributes
  • Reset form after successful submission to clear inputs
  • Save form state to localStorage for long forms
  • Use TypeScript for better type safety with form data

Form Libraries Comparison

LibraryBundle SizePerformanceFeaturesLearning Curve
React Hook Form~9KBExcellentUncontrolled by default, minimal re-rendersLow
Formik~12KBGoodFull-featured, controlledLow
React Final Form~8KBGoodSubscription-based, performantMedium
React Forms (native)0KBGoodBasic, requires custom codeLow

Common Mistakes

React JSXRead-only
1
// ❌ Wrong: Not handling form submission
<form>
  <input />
  <button type="submit">Submit</button>
</form>

// ✅ Correct: Handle submit and prevent default
<form onSubmit={handleSubmit}>
  <input />
  <button type="submit">Submit</button>
</form>

// ❌ Wrong: Not providing name attributes
<input value={username} onChange={handleChange} />

// ✅ Correct: Include name attribute
<input name="username" value={username} onChange={handleChange} />

// ❌ Wrong: Mutating state directly
const handleChange = (e) => {
  formData[e.target.name] = e.target.value; // Direct mutation
  setFormData(formData);
};

// ✅ Correct: Create new object
const handleChange = (e) => {
  setFormData({ ...formData, [e.target.name]: e.target.value });
};

// ❌ Wrong: Not resetting form after submit
const handleSubmit = async (data) => {
  await submitData(data);
  // Form still has old data
};

// ✅ Correct: Reset after submission
const handleSubmit = async (data) => {
  await submitData(data);
  resetForm();
};

Conclusion

Forms are a critical part of most React applications. Controlled components give you full control over form state and validation. For complex forms, consider using libraries like React Hook Form for better performance and developer experience. Always implement proper validation, error handling, and accessibility.

Try it yourself

import { useState } from 'react';

function RegistrationForm() {
  const [formData, setFormData] = useState({
    fullName: '',
    email: '',
    password: '',
    confirmPassword: '',
    role: 'user',
    newsletter: false
  });
  
  const [errors, setErrors] = useState({});
  const [submitted, setSubmitted] = useState(false);

  const validateForm = () => {
    const newErrors = {};
    
    if (!formData.fullName.trim()) {
      newErrors.fullName = 'Full name is required';
    } else if (formData.fullName.length < 2) {
      newErrors.fullName = 'Name must be at least 2 characters';
    }
    
    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Email is invalid';
    }
    
    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length < 6) {
      newErrors.password = 'Password must be at least 6 characters';
    }
    
    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'Passwords do not match';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    });
    // Clear error for this field when user starts typing
    if (errors[name]) {
      setErrors({ ...errors, [name]: '' });
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validateForm()) {
      console.log('Form submitted:', formData);
      setSubmitted(true);
      // Reset form after 3 seconds
      setTimeout(() => {
        setFormData({
          fullName: '',
          email: '',
          password: '',
          confirmPassword: '',
          role: 'user',
          newsletter: false
        });
        setSubmitted(false);
      }, 3000);
    }
  };

  if (submitted) {
    return (
      <div style={{ textAlign: 'center', padding: '40px' }}>
        <h2>🎉 Registration Successful!</h2>
        <p>Welcome, {formData.fullName}!</p>
        <p>Check your email at {formData.email} for confirmation.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>Register</h2>
      
      <div style={{ marginBottom: '15px' }}>
        <label>Full Name *</label>
        <input
          type="text"
          name="fullName"
          value={formData.fullName}
          onChange={handleChange}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.fullName ? '1px solid red' : '1px solid #ddd',
            borderRadius: '4px'
          }}
        />
        {errors.fullName && <span style={{ color: 'red', fontSize: '14px' }}>{errors.fullName}</span>}
      </div>

      <div style={{ marginBottom: '15px' }}>
        <label>Email *</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.email ? '1px solid red' : '1px solid #ddd',
            borderRadius: '4px'
          }}
        />
        {errors.email && <span style={{ color: 'red', fontSize: '14px' }}>{errors.email}</span>}
      </div>

      <div style={{ marginBottom: '15px' }}>
        <label>Password *</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.password ? '1px solid red' : '1px solid #ddd',
            borderRadius: '4px'
          }}
        />
        {errors.password && <span style={{ color: 'red', fontSize: '14px' }}>{errors.password}</span>}
      </div>

      <div style={{ marginBottom: '15px' }}>
        <label>Confirm Password *</label>
        <input
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleChange}
          style={{
            width: '100%',
            padding: '8px',
            border: errors.confirmPassword ? '1px solid red' : '1px solid #ddd',
            borderRadius: '4px'
          }}
        />
        {errors.confirmPassword && <span style={{ color: 'red', fontSize: '14px' }}>{errors.confirmPassword}</span>}
      </div>

      <div style={{ marginBottom: '15px' }}>
        <label>Role</label>
        <select
          name="role"
          value={formData.role}
          onChange={handleChange}
          style={{ width: '100%', padding: '8px' }}
        >
          <option value="user">User</option>
          <option value="admin">Admin</option>
          <option value="moderator">Moderator</option>
        </select>
      </div>

      <div style={{ marginBottom: '20px' }}>
        <label>
          <input
            type="checkbox"
            name="newsletter"
            checked={formData.newsletter}
            onChange={handleChange}
          />
          Subscribe to newsletter
        </label>
      </div>

      <button
        type="submit"
        style={{
          width: '100%',
          padding: '10px',
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer'
        }}
      >
        Register
      </button>
    </form>
  );
}

export default RegistrationForm;

Test Your Knowledge

Q1
of 4

What is a controlled component?

A
Component with internal state
B
Component controlled by parent via props
C
Component where form data is handled by React state
D
Component with refs
Q2
of 4

How do you access form values in uncontrolled components?

A
State
B
Props
C
Refs
D
Context
Q3
of 4

Which library is recommended for complex forms?

A
Redux Form
B
React Hook Form
C
React Router
D
Axios
Q4
of 4

How to prevent form submission page reload?

A
return false
B
event.stopPropagation()
C
event.preventDefault()
D
event.stopReload()

Frequently Asked Questions

Controlled vs uncontrolled components?

Controlled: React state controls input value. Uncontrolled: DOM handles value with refs.

When to use React Hook Form?

For complex forms, better performance, and less boilerplate code.

How to validate forms?

Client-side validation with custom logic or libraries like Yup, Zod.

How to handle file uploads?

Use uncontrolled input with type='file' and FormData API.

Previous

react lists keys

Next

react hooks intro

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.