React BasicsJanuary 13, 2025

Form Handling in React - Complete Guide

Learn how to handle forms in React. Understand controlled components, form validation, and best practices for building forms.

Author: Sushil Kumar

reactformsvalidationcontrolled-components

Form Handling in React - Complete Guide

Forms are everywhere in web applications. Whether it's a login form, contact form, or search box, you'll need to handle forms in React. The good news? React makes it pretty straightforward once you understand the basics!

Controlled Components

In React, form data is usually handled by component state. This is called a "controlled component" - React controls the input's value.

function NameForm() {
  const [name, setName] = useState('');
 
  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`Submitted: ${name}`);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

The key here is that value={name} and onChange work together. The input's value comes from state, and when it changes, we update the state. This creates a two-way data flow!

Multiple Inputs

When you have multiple inputs, you can handle them all with one state object:

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
 
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
 
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Notice how we use name attributes to identify which input changed. This pattern scales well for forms with many inputs!

Different Input Types

React handles all HTML input types. Here are the most common ones:

Text Input

function TextInput() {
  const [text, setText] = useState('');
  return (
    <input
      type="text"
      value={text}
      onChange={(e) => setText(e.target.value)}
    />
  );
}

Textarea

function TextareaInput() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={(e) => setText(e.target.value)}
    />
  );
}

Works exactly like a text input!

Select Dropdown

function SelectInput() {
  const [selected, setSelected] = useState('');
 
  return (
    <select value={selected} onChange={(e) => setSelected(e.target.value)}>
      <option value="">Choose...</option>
      <option value="option1">Option 1</option>
      <option value="option2">Option 2</option>
      <option value="option3">Option 3</option>
    </select>
  );
}

Checkbox

Checkboxes are a bit different - they use checked instead of value:

function CheckboxInput() {
  const [checked, setChecked] = useState(false);
 
  return (
    <label>
      <input
        type="checkbox"
        checked={checked}
        onChange={(e) => setChecked(e.target.checked)}
      />
      I agree to the terms
    </label>
  );
}

Notice e.target.checked instead of e.target.value. That's the key difference!

Radio Buttons

Radio buttons need special handling too:

function RadioInput() {
  const [selected, setSelected] = useState('');
 
  return (
    <div>
      <label>
        <input
          type="radio"
          value="option1"
          checked={selected === 'option1'}
          onChange={(e) => setSelected(e.target.value)}
        />
        Option 1
      </label>
      <label>
        <input
          type="radio"
          value="option2"
          checked={selected === 'option2'}
          onChange={(e) => setSelected(e.target.value)}
        />
        Option 2
      </label>
    </div>
  );
}

All radio buttons share the same state value, but only the checked one matches it.

Form Validation

Validation is super important for good user experience. Here's how to do it:

Basic Validation

function ValidatedForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState({});
 
  const validate = () => {
    const newErrors = {};
 
    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';
    }
 
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
 
  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      console.log('Form is valid!', formData);
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      <div>
        <input
          type="password"
          value={formData.password}
          onChange={(e) => setFormData({ ...formData, password: e.target.value })}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

This validates on submit. Show errors, and only submit if everything is valid!

Real-time Validation

You can also validate as the user types:

function RealTimeValidation() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
 
  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);
 
    if (value && !/\S+@\S+\.\S+/.test(value)) {
      setError('Email is invalid');
    } else {
      setError('');
    }
  };
 
  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={handleChange}
      />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

This gives immediate feedback, which users love!

Form Submission

Here's how to submit a form properly:

function SubmitForm() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [message, setMessage] = useState('');
 
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
 
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });
 
      if (response.ok) {
        setMessage('Successfully submitted!');
        setFormData({ name: '', email: '' });
      }
    } catch (error) {
      setMessage('An error occurred');
    } finally {
      setIsSubmitting(false);
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
      />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
      {message && <p>{message}</p>}
    </form>
  );
}

Notice how we:

  • Prevent default form submission
  • Show loading state
  • Handle success and error
  • Disable button while submitting

Best Practices

1. Use Controlled Components

// Good - React controls the value
<input value={value} onChange={handleChange} />
 
// Avoid - Uncontrolled (unless you really need it)
<input ref={inputRef} />

Controlled components give you more control and make validation easier.

2. Use Name Attributes

Always use name attributes for multiple inputs:

<input
  name="email"
  value={formData.email}
  onChange={handleChange}
/>

This makes it easy to handle multiple inputs with one handler.

3. Validate Early

Validate on blur or as user types, not just on submit:

const handleBlur = () => {
  // Validate when user leaves the field
  validateField();
};

Better user experience!

4. Disable Submit Button

Disable the submit button when form is invalid or submitting:

<button
  type="submit"
  disabled={!isValid || isSubmitting}
>
  Submit
</button>

Prevents double submissions and shows clear feedback.

Common Patterns

Handling Multiple Checkboxes

function CheckboxGroup() {
  const [selected, setSelected] = useState([]);
 
  const handleChange = (value) => {
    setSelected(prev => 
      prev.includes(value)
        ? prev.filter(item => item !== value)
        : [...prev, value]
    );
  };
 
  return (
    <div>
      {['option1', 'option2', 'option3'].map(option => (
        <label key={option}>
          <input
            type="checkbox"
            checked={selected.includes(option)}
            onChange={() => handleChange(option)}
          />
          {option}
        </label>
      ))}
    </div>
  );
}

File Upload

function FileUpload() {
  const [file, setFile] = useState(null);
 
  const handleChange = (e) => {
    setFile(e.target.files[0]);
  };
 
  return (
    <input
      type="file"
      onChange={handleChange}
    />
  );
}

Common Mistakes

Here's what to avoid:

  1. Forgetting preventDefault - Forms will refresh the page!
  2. Not using controlled components - Makes validation harder
  3. Not validating - Users can submit invalid data
  4. Not showing errors - Users don't know what's wrong
  5. Not disabling submit - Allows double submissions

Visual Explanation: Controlled Components Flow

Here's how controlled components work:

User Types in Input:
┌─────────────────────────────────┐
│ User types "Hello"              │
│                                 │
│ Input onChange fires            │
│ setValue("Hello") called        │
│                                 │
│ State updates:                  │
│ value = "Hello"                 │
│                                 │
│ Component re-renders            │
│ Input shows "Hello"             │
└─────────────────────────────────┘
 
Two-Way Data Binding:
┌──────────────┐         ┌──────────────┐
│   State      │◄────────┤   Input     │
│   value      │         │   value     │
└──────┬───────┘         └──────▲──────┘
       │                        │
       │ onChange updates       │
       │                        │
       └────────────────────────┘

Form Validation Flow

User Submits Form


┌──────────────┐
│  Validate   │
│   Inputs    │
└──────┬──────┘

   ┌───┴───┐
   │       │
Valid?   Invalid?
   │       │
   ▼       ▼
Submit   Show Errors
Form     to User

Frequently Asked Questions (FAQ)

Q1: What's the difference between controlled and uncontrolled components?

A:

  • Controlled - React controls the value via state (recommended)
  • Uncontrolled - Browser/DOM controls the value via refs

Use controlled components for most cases - they're easier to validate and manage.

Q2: Do I always need to use controlled components?

A: For most forms, yes! Controlled components give you:

  • Easy validation
  • Better control
  • Easier testing
  • Consistent behavior

Uncontrolled components are only useful in special cases.

Q3: How do I handle file uploads in React?

A: File inputs are always uncontrolled:

function FileUpload() {
  const fileRef = useRef(null);
  
  const handleSubmit = () => {
    const file = fileRef.current.files[0];
    // Handle file
  };
  
  return <input ref={fileRef} type="file" />;
}

Q4: Can I use HTML5 validation with React?

A: Yes! You can use both:

<input
  type="email"
  required
  pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
  value={email}
  onChange={handleChange}
/>

But React validation gives you more control and better error messages.

Q5: How do I reset a form after submission?

A: Reset the state:

const [formData, setFormData] = useState({ name: '', email: '' });
 
const handleSubmit = (e) => {
  e.preventDefault();
  // Submit form
  setFormData({ name: '', email: '' }); // Reset
};

Q6: Should I validate on change or on submit?

A: Both! Validate on blur for immediate feedback, and validate on submit to catch everything. Real-time validation (on change) can be annoying for users.

Q7: How do I handle multiple checkboxes?

A: Use an array in state:

const [selected, setSelected] = useState([]);
 
const handleCheckbox = (value) => {
  setSelected(prev =>
    prev.includes(value)
      ? prev.filter(item => item !== value)
      : [...prev, value]
  );
};

Q8: Can I use form libraries like Formik?

A: Yes! Formik and React Hook Form are popular. They handle a lot of form logic for you. But learn the basics first, then use libraries when forms get complex.

Q9: How do I prevent form submission?

A: Use e.preventDefault():

const handleSubmit = (e) => {
  e.preventDefault(); // Prevents page reload
  // Your form logic
};

Q10: What's the best way to handle form errors?

A:

  1. Store errors in state
  2. Show errors near the input
  3. Highlight invalid inputs
  4. Disable submit if form is invalid
  5. Show summary of errors

Always be helpful and clear about what's wrong!


Next Steps

Now that you understand forms, here's what to learn next:

Happy coding! 🚀