Forms & Controlled Components

Handling form inputs and controlled components

Forms in React

HTML form elements work differently in React because they naturally maintain their own state. React gives you two approaches: controlled components (React controls the form) and uncontrolled components (DOM controls the form).

Controlled vs Uncontrolled

Controlled (Recommended)

React state is the "single source of truth". Input value is controlled by React.

Uncontrolled

DOM handles the form data. Use refs to access values when needed.

Text Input (Controlled)

import { useState } from 'react';

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

  const handleChange = (e) => {
    setName(e.target.value);
  };

  return (
    <div>
      <label htmlFor="name">Name:</label>
      <input
        type="text"
        id="name"
        value={name}           {/* Controlled by state */}
        onChange={handleChange} {/* Updates state on change */}
        placeholder="Enter your name"
      />
      <p>Hello, {name || 'stranger'}!</p>
    </div>
  );
}

Multiple Inputs

function MultipleInputs() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: ''
  });

  // Single handler for all inputs
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value  // Computed property name
    }));
  };

  return (
    <form>
      <input
        name="firstName"
        value={formData.firstName}
        onChange={handleChange}
        placeholder="First name"
      />
      <input
        name="lastName"
        value={formData.lastName}
        onChange={handleChange}
        placeholder="Last name"
      />
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
    </form>
  );
}

Different Input Types

function AllInputTypes() {
  const [form, setForm] = useState({
    text: '',
    password: '',
    number: 0,
    email: '',
    date: '',
    color: '#000000'
  });

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

  return (
    <form>
      <input type="text" name="text" value={form.text} onChange={handleChange} />
      <input type="password" name="password" value={form.password} onChange={handleChange} />
      <input type="number" name="number" value={form.number} onChange={handleChange} />
      <input type="email" name="email" value={form.email} onChange={handleChange} />
      <input type="date" name="date" value={form.date} onChange={handleChange} />
      <input type="color" name="color" value={form.color} onChange={handleChange} />
    </form>
  );
}

Textarea

function TextareaExample() {
  const [bio, setBio] = useState('');

  return (
    <div>
      <label htmlFor="bio">Bio:</label>
      <textarea
        id="bio"
        value={bio}
        onChange={(e) => setBio(e.target.value)}
        rows={4}
        placeholder="Tell us about yourself"
      />
      <p>Characters: {bio.length}/500</p>
    </div>
  );
}

Select (Dropdown)

function SelectExample() {
  const [country, setCountry] = useState('');
  const [languages, setLanguages] = useState([]);

  return (
    <div>
      {/* Single select */}
      <select value={country} onChange={(e) => setCountry(e.target.value)}>
        <option value="">Select a country</option>
        <option value="us">United States</option>
        <option value="uk">United Kingdom</option>
        <option value="ca">Canada</option>
      </select>

      {/* Multiple select */}
      <select
        multiple
        value={languages}
        onChange={(e) => {
          const selected = Array.from(e.target.selectedOptions, opt => opt.value);
          setLanguages(selected);
        }}
      >
        <option value="js">JavaScript</option>
        <option value="py">Python</option>
        <option value="go">Go</option>
      </select>
    </div>
  );
}

Checkbox

function CheckboxExample() {
  const [isAgree, setIsAgree] = useState(false);
  const [interests, setInterests] = useState([]);

  // Toggle single checkbox
  const handleAgreeChange = (e) => {
    setIsAgree(e.target.checked);
  };

  // Handle multiple checkboxes
  const handleInterestChange = (e) => {
    const { value, checked } = e.target;
    if (checked) {
      setInterests(prev => [...prev, value]);
    } else {
      setInterests(prev => prev.filter(i => i !== value));
    }
  };

  return (
    <div>
      {/* Single checkbox */}
      <label>
        <input
          type="checkbox"
          checked={isAgree}
          onChange={handleAgreeChange}
        />
        I agree to the terms
      </label>

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

Radio Buttons

function RadioExample() {
  const [plan, setPlan] = useState('free');

  return (
    <div>
      <p>Select a plan:</p>
      
      <label>
        <input
          type="radio"
          name="plan"
          value="free"
          checked={plan === 'free'}
          onChange={(e) => setPlan(e.target.value)}
        />
        Free
      </label>
      
      <label>
        <input
          type="radio"
          name="plan"
          value="pro"
          checked={plan === 'pro'}
          onChange={(e) => setPlan(e.target.value)}
        />
        Pro ($9/month)
      </label>
      
      <label>
        <input
          type="radio"
          name="plan"
          value="enterprise"
          checked={plan === 'enterprise'}
          onChange={(e) => setPlan(e.target.value)}
        />
        Enterprise ($99/month)
      </label>

      <p>Selected plan: {plan}</p>
    </div>
  );
}

Form Submission

function LoginForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });
  const [isSubmitting, setIsSubmitting] = useState(false);

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

  const handleSubmit = async (e) => {
    e.preventDefault();  // Prevent page reload!
    
    setIsSubmitting(true);
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });
      
      if (response.ok) {
        console.log('Login successful!');
      }
    } catch (error) {
      console.error('Login failed:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        required
      />
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        required
      />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

Form Validation

function ValidatedForm() {
  const [email, setEmail] = useState('');
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const validate = () => {
    const newErrors = {};
    
    if (!email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(email)) {
      newErrors.email = 'Invalid email format';
    }
    
    return newErrors;
  };

  const handleBlur = () => {
    setTouched({ email: true });
    setErrors(validate());
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const validationErrors = validate();
    setErrors(validationErrors);
    setTouched({ email: true });

    if (Object.keys(validationErrors).length === 0) {
      console.log('Form is valid!');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={handleBlur}
        className={touched.email && errors.email ? 'error' : ''}
      />
      {touched.email && errors.email && (
        <span className="error-message">{errors.email}</span>
      )}
      <button type="submit">Submit</button>
    </form>
  );
}

Uncontrolled Components (with useRef)

import { useRef } from 'react';

function UncontrolledForm() {
  const inputRef = useRef(null);
  const fileRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Input value:', inputRef.current.value);
    console.log('File:', fileRef.current.files[0]);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} type="text" defaultValue="Initial value" />
      <input ref={fileRef} type="file" />
      <button type="submit">Submit</button>
    </form>
  );
}

🎯 Forms Best Practices

  • ✓ Use controlled components for most forms
  • ✓ Always call e.preventDefault() on form submit
  • ✓ Use a single handler for multiple inputs with name attribute
  • ✓ Validate on blur and on submit
  • ✓ Show validation errors near the input
  • ✓ Disable submit button while submitting
  • ✓ Consider form libraries (React Hook Form, Formik) for complex forms