Skip to content
beginnerPhase 36 · React

React Forms

Build controlled and uncontrolled forms with proper state management.

45m
0 problems
Topic Progress0%

Controlled vs Uncontrolled Forms

React offers two ways to handle form inputs. Understanding the difference is essential.

Controlled Inputs

The input value is controlled by React state.

import { useState } from 'react';

function ControlledForm() {
  const [name, setName] = useState('');

  return (
    <form>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <p>Hello, {name || 'Stranger'}!</p>
    </form>
  );
}

Characteristics:

  • Input value stored in state
  • State is the single source of truth
  • Updates on every keystroke
  • Easy to validate and control

Uncontrolled Inputs

The input manages its own state internally.

function UncontrolledForm() {
  const nameRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    alert('Name: ' + nameRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={nameRef} defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

Characteristics:

  • DOM holds the value
  • Use refs to access value
  • Simpler for simple forms
  • Harder to validate

When to Use Which

Use Case Approach
Form validation needed Controlled
Real-time field dependencies Controlled
Simple form, minimal JS Uncontrolled
Integrating with non-React code Uncontrolled
File uploads Uncontrolled
Complex multi-step forms Controlled

Default Values

// Controlled: set initial state
const [email, setEmail] = useState('user@example.com');
<input value={email} onChange={(e) => setEmail(e.target.value)} />

// Uncontrolled: use defaultValue
<input type="text" defaultValue="user@example.com" ref={inputRef} />

Handling Form Submission

Forms need to submit data to a server or process it with JavaScript.

Basic Form Handler

import { useState } from 'react';

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });

  const handleSubmit = (e) => {
    e.preventDefault(); // Prevent page reload
    console.log('Form submitted:', formData);
    // Send to API
    fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData)
    });
  };

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

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Send</button>
    </form>
  );
}

Why preventDefault?

// Without preventDefault: page reloads
// With preventDefault: we control what happens
const handleSubmit = (e) => {
  e.preventDefault(); // Stop the default behavior
  // Now we can:
  // - Validate
  // - Send to API
  // - Update state
  // - Show success message
};

Submission States

function ContactForm() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const [status, setStatus] = useState('idle'); // idle, submitting, success, error

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('submitting');
    
    try {
      await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(formData)
      });
      setStatus('success');
    } catch (err) {
      setStatus('error');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <button disabled={status === 'submitting'}>
        {status === 'submitting' ? 'Sending...' : 'Send'}
      </button>
      {status === 'success' && <p>Message sent!</p>}
      {status === 'error' && <p>Something went wrong.</p>}
    </form>
  );
}

Managing Multiple Fields

For forms with many fields, use a single state object instead of multiple useState calls.

Single Object State

function RegistrationForm() {
  const [form, setForm] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: ''
  });

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

  return (
    <form>
      <input name="firstName" value={form.firstName} onChange={handleChange} />
      <input name="lastName" value={form.lastName} onChange={handleChange} />
      <input name="email" type="email" value={form.email} onChange={handleChange} />
      <input name="password" type="password" value={form.password} onChange={handleChange} />
      <input name="confirmPassword" type="password" value={form.confirmPassword} onChange={handleChange} />
    </form>
  );
}

Dynamic Field Updates

// Update nested fields
const [form, setForm] = useState({
  name: '',
  address: {
    street: '',
    city: '',
    zip: ''
  }
});

// For nested fields, create a helper
const updateNestedField = (section, field, value) => {
  setForm(prev => ({
    ...prev,
    [section]: { ...prev[section], [field]: value }
  }));
};

// Usage
<input
  value={form.address.city}
  onChange={(e) => updateNestedField('address', 'city', e.target.value)}
/>

Array Fields (Dynamic List)

function SkillsForm() {
  const [skills, setSkills] = useState(['JavaScript', 'React']);

  const addSkill = () => {
    setSkills(prev => [...prev, '']);
  };

  const updateSkill = (index, value) => {
    setSkills(prev => prev.map((skill, i) => i === index ? value : skill));
  };

  const removeSkill = (index) => {
    setSkills(prev => prev.filter((_, i) => i !== index));
  };

  return (
    <div>
      {skills.map((skill, index) => (
        <div key={index}>
          <input
            value={skill}
            onChange={(e) => updateSkill(index, e.target.value)}
          />
          <button type="button" onClick={() => removeSkill(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={addSkill}>Add Skill</button>
    </div>
  );
}

Form Validation

React forms need custom validation since controlled inputs bypass HTML5 validation.

Basic Validation

function LoginForm() {
  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) {
      newErrors.password = 'Password is required';
    } else if (form.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      // Submit form
    }
  };

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

Real-Time Validation

// Validate on change (real-time feedback)
const handleChange = (e) => {
  const { name, value } = e.target;
  setForm(prev => ({ ...prev, [name]: value }));
  
  // Clear error when user starts typing
  if (errors[name]) {
    setErrors(prev => ({ ...prev, [name]: '' }));
  }
  
  // Validate field immediately
  const fieldError = validateField(name, value);
  if (fieldError) {
    setErrors(prev => ({ ...prev, [name]: fieldError }));
  }
};

// Or validate on blur (when user leaves field)
const handleBlur = (e) => {
  const { name, value } = e.target;
  const fieldError = validateField(name, value);
  setErrors(prev => ({ ...prev, [name]: fieldError }));
};

Validation Rules Object

const validationRules = {
  email: {
    required: true,
    pattern: /\S+@\S+\.\S+/,
    message: 'Please enter a valid email'
  },
  password: {
    required: true,
    minLength: 8,
    message: 'Password must be at least 8 characters'
  },
  age: {
    required: true,
    min: 18,
    max: 120,
    message: 'Age must be between 18 and 120'
  }
};

const validateField = (name, value) => {
  const rules = validationRules[name];
  if (!rules) return '';
  
  if (rules.required && !value) return 'This field is required';
  if (rules.pattern && !rules.pattern.test(value)) return rules.message;
  if (rules.minLength && value.length < rules.minLength) return rules.message;
  if (rules.min && Number(value) < rules.min) return rules.message;
  if (rules.max && Number(value) > rules.max) return rules.message;
  
  return '';
};

Advanced Form Patterns

Common patterns for building complex forms in React.

Form Component Pattern

// Reusable FormField component
function FormField({ label, name, error, children }) {
  return (
    <div className="form-field">
      <label htmlFor={name}>{label}</label>
      {children}
      {error && <span className="error" role="alert">{error}</span>}
    </div>
  );
}

// Usage
<FormField label="Email" name="email" error={errors.email}>
  <input
    id="email"
    name="email"
    type="email"
    value={form.email}
    onChange={handleChange}
  />
</FormField>

Select and Checkbox Handling

function PreferencesForm() {
  const [form, setForm] = useState({
    newsletter: false,
    theme: 'light',
    interests: []
  });

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

  const handleInterestChange = (e) => {
    const { value, checked } = e.target;
    setForm(prev => ({
      ...prev,
      interests: checked
        ? [...prev.interests, value]
        : prev.interests.filter(i => i !== value)
    }));
  };

  return (
    <form>
      {/* Checkbox */}
      <label>
        <input
          type="checkbox"
          name="newsletter"
          checked={form.newsletter}
          onChange={handleChange}
        />
        Subscribe to newsletter
      </label>

      {/* Select */}
      <select name="theme" value={form.theme} onChange={handleChange}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>

      {/* Multiple checkboxes */}
      <div>
        <label>
          <input type="checkbox" value="tech" checked={form.interests.includes('tech')} onChange={handleInterestChange} />
          Technology
        </label>
        <label>
          <input type="checkbox" value="sports" checked={form.interests.includes('sports')} onChange={handleInterestChange} />
          Sports
        </label>
      </div>
    </form>
  );
}

Form Reset

function ContactForm() {
  const initialForm = { name: '', email: '', message: '' };
  const [form, setForm] = useState(initialForm);

  const handleSubmit = (e) => {
    e.preventDefault();
    // submit...
    setForm(initialForm); // Reset to initial values
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <button type="submit">Send</button>
      <button type="button" onClick={() => setForm(initialForm)}>Reset</button>
    </form>
  );
}

Multi-Step Form

function MultiStepForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({});

  const nextStep = () => setStep(s => s + 1);
  const prevStep = () => setStep(s => s - 1);

  return (
    <div>
      {step === 1 && <Step1 data={formData} setData={setFormData} onNext={nextStep} />}
      {step === 2 && <Step2 data={formData} setData={setFormData} onNext={nextStep} onBack={prevStep} />}
      {step === 3 && <Step3 data={formData} onBack={prevStep} onSubmit={handleSubmit} />}
    </div>
  );
}

Practice Problems

0/3solved
Build React Forms Component

Create a reusable React component implementing React Forms. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
React Forms Testing

Write unit and integration tests for React Forms using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
React Forms Performance

Optimize React Forms for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. What is a controlled input in React?

Question 1 options

2. Why do we call e.preventDefault() in form submission?

Question 2 options

3. When should you use an uncontrolled input?

Question 3 options

4. What is the advantage of using a single state object for form fields?

Question 4 options

5. How do you handle checkbox changes in React?

Question 5 options

Flashcards

Question

What is the difference between controlled and uncontrolled inputs?

Answer

Controlled: value stored in React state, updated via onChange. Uncontrolled: value managed by DOM, accessed via refs. Controlled is better for validation; uncontrolled is simpler.

Question

Why must you call e.preventDefault() in onSubmit?

Answer

Without it, the browser submits the form and reloads the page. preventDefault lets you handle submission with JavaScript instead.

Question

How do you handle multiple checkboxes in React?

Answer

Store selected values as an array in state. On change, add the value if checked, or filter it out if unchecked.

Question

What is the pattern for a reusable FormField component?

Answer

A component that wraps an input with a label and error message, accepting name, label, error, and children props.

Question

What is React Forms?

Answer

React Forms is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Use controlled inputs when you need validation or field dependencies
  • 2.Use uncontrolled inputs for simple forms or non-React integration
  • 3.Always call e.preventDefault() in onSubmit to handle submission yourself
  • 4.Group related form fields in a single state object for easier management
  • 5.Validate on blur for better UX, or on change for real-time feedback
  • 6.Create reusable FormField components for consistency

Interview Tips

  • Explain the difference between controlled and uncontrolled inputs
  • Know how to handle form submission with preventDefault
  • Be able to build a form with real-time validation
  • Understand how to manage dynamic form fields (add/remove items)

Cheat Sheet

React Forms Cheat Sheet

// Controlled input
const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />

// Form submission
const handleSubmit = (e) => {
  e.preventDefault();
  // process form
};
<form onSubmit={handleSubmit}>

// Multiple fields
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = (e) => {
  setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};