javascript-today

Forms in React

Controlled Components

In React, form inputs are “controlled” by component state - React controls the input value:

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

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

Controlled = React state is the “single source of truth”

  • Input value comes from state
  • Input onChange updates state
  • State controls what user sees

Text Inputs

Basic Text Input

function EmailForm() {
  const [email, setEmail] = useState('');

  function handleSubmit(event) {
    event.preventDefault();
    console.log('Email submitted:', email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email:
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter your email"
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

Textarea

function CommentForm() {
  const [comment, setComment] = useState('');

  return (
    <form>
      <label>
        Comment:
        <textarea
          value={comment}
          onChange={(e) => setComment(e.target.value)}
          rows={4}
          placeholder="Write your comment..."
        />
      </label>
      <p>Character count: {comment.length}</p>
    </form>
  );
}

Select Dropdowns

Basic Select

function CountrySelect() {
  const [country, setCountry] = useState('usa');

  return (
    <form>
      <label>
        Country:
        <select value={country} onChange={(e) => setCountry(e.target.value)}>
          <option value="usa">United States</option>
          <option value="uk">United Kingdom</option>
          <option value="canada">Canada</option>
          <option value="australia">Australia</option>
        </select>
      </label>
      <p>Selected: {country}</p>
    </form>
  );
}

Dynamic Options

function ProductSelector() {
  const [selectedId, setSelectedId] = useState('');
  const products = [
    { id: '1', name: 'Laptop', price: 999 },
    { id: '2', name: 'Mouse', price: 29 },
    { id: '3', name: 'Keyboard', price: 79 }
  ];

  return (
    <form>
      <select value={selectedId} onChange={(e) => setSelectedId(e.target.value)}>
        <option value="">-- Select Product --</option>
        {products.map(product => (
          <option key={product.id} value={product.id}>
            {product.name} (${product.price})
          </option>
        ))}
      </select>
    </form>
  );
}

Checkboxes

Single Checkbox

function TermsCheckbox() {
  const [agreed, setAgreed] = useState(false);

  return (
    <form>
      <label>
        <input
          type="checkbox"
          checked={agreed}
          onChange={(e) => setAgreed(e.target.checked)}
        />
        I agree to the terms and conditions
      </label>
      <button type="submit" disabled={!agreed}>
        Submit
      </button>
    </form>
  );
}

Multiple Checkboxes

function InterestsForm() {
  const [interests, setInterests] = useState([]);

  function handleChange(interest) {
    if (interests.includes(interest)) {
      // Remove if already selected
      setInterests(interests.filter(i => i !== interest));
    } else {
      // Add if not selected
      setInterests([...interests, interest]);
    }
  }

  return (
    <form>
      <p>Select your interests:</p>
      <label>
        <input
          type="checkbox"
          checked={interests.includes('sports')}
          onChange={() => handleChange('sports')}
        />
        Sports
      </label>
      <label>
        <input
          type="checkbox"
          checked={interests.includes('music')}
          onChange={() => handleChange('music')}
        />
        Music
      </label>
      <label>
        <input
          type="checkbox"
          checked={interests.includes('reading')}
          onChange={() => handleChange('reading')}
        />
        Reading
      </label>
      <p>Selected: {interests.join(', ') || 'None'}</p>
    </form>
  );
}

Radio Buttons

function SizeSelector() {
  const [size, setSize] = useState('medium');

  return (
    <form>
      <p>Select size:</p>
      <label>
        <input
          type="radio"
          value="small"
          checked={size === 'small'}
          onChange={(e) => setSize(e.target.value)}
        />
        Small
      </label>
      <label>
        <input
          type="radio"
          value="medium"
          checked={size === 'medium'}
          onChange={(e) => setSize(e.target.value)}
        />
        Medium
      </label>
      <label>
        <input
          type="radio"
          value="large"
          checked={size === 'large'}
          onChange={(e) => setSize(e.target.value)}
        />
        Large
      </label>
      <p>Selected: {size}</p>
    </form>
  );
}

Multiple Inputs - Single State Object

Handle multiple form fields efficiently:

function SignupForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    age: '',
    country: 'usa',
    newsletter: false
  });

  function handleChange(event) {
    const { name, value, type, checked } = event.target;
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    });
  }

  function handleSubmit(event) {
    event.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="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Password"
      />
      
      <input
        type="number"
        name="age"
        value={formData.age}
        onChange={handleChange}
        placeholder="Age"
      />
      
      <select name="country" value={formData.country} onChange={handleChange}>
        <option value="usa">USA</option>
        <option value="uk">UK</option>
        <option value="canada">Canada</option>
      </select>
      
      <label>
        <input
          type="checkbox"
          name="newsletter"
          checked={formData.newsletter}
          onChange={handleChange}
        />
        Subscribe to newsletter
      </label>
      
      <button type="submit">Sign Up</button>
    </form>
  );
}

Form Validation

Client-Side Validation

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

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

  function handleSubmit(event) {
    event.preventDefault();
    
    const newErrors = validate();
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }
    
    setErrors({});
    console.log('Valid form:', { email, password });
    // Submit to API
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
        />
        {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      </div>
      
      <div>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
        />
        {errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
      </div>
      
      <button type="submit">Log In</button>
    </form>
  );
}

Real-Time Validation

function EmailInput() {
  const [email, setEmail] = useState('');
  const [touched, setTouched] = useState(false);

  const isValid = /\S+@\S+\.\S+/.test(email);
  const showError = touched && !isValid && email.length > 0;

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={() => setTouched(true)}
        placeholder="Email"
        style={{ borderColor: showError ? 'red' : 'gray' }}
      />
      {showError && <p style={{ color: 'red' }}>Invalid email address</p>}
    </div>
  );
}

Form Submission

Basic Form Submission

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitStatus, setSubmitStatus] = useState(null);

  function handleChange(event) {
    const { name, value } = event.target;
    setFormData({ ...formData, [name]: value });
  }

  async function handleSubmit(event) {
    event.preventDefault();
    setIsSubmitting(true);
    setSubmitStatus(null);

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      if (response.ok) {
        setSubmitStatus('success');
        setFormData({ name: '', email: '', message: '' });
      } else {
        setSubmitStatus('error');
      }
    } catch (error) {
      setSubmitStatus('error');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
        required
      />
      
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
        required
      />
      
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
        required
      />
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </button>
      
      {submitStatus === 'success' && (
        <p style={{ color: 'green' }}>Message sent successfully!</p>
      )}
      {submitStatus === 'error' && (
        <p style={{ color: 'red' }}>Error sending message. Please try again.</p>
      )}
    </form>
  );
}

File Uploads

function FileUploadForm() {
  const [file, setFile] = useState(null);
  const [preview, setPreview] = useState(null);

  function handleFileChange(event) {
    const selectedFile = event.target.files[0];
    setFile(selectedFile);

    // Create preview for images
    if (selectedFile && selectedFile.type.startsWith('image/')) {
      const reader = new FileReader();
      reader.onloadend = () => {
        setPreview(reader.result);
      };
      reader.readAsDataURL(selectedFile);
    }
  }

  async function handleSubmit(event) {
    event.preventDefault();
    
    if (!file) return;

    const formData = new FormData();
    formData.append('file', file);

    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData  // Don't set Content-Type, browser will set it
      });
      
      if (response.ok) {
        console.log('File uploaded successfully');
      }
    } catch (error) {
      console.error('Upload failed:', error);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="file"
        onChange={handleFileChange}
        accept="image/*"
      />
      
      {preview && (
        <div>
          <p>Preview:</p>
          <img src={preview} alt="Preview" style={{ maxWidth: '200px' }} />
        </div>
      )}
      
      <button type="submit" disabled={!file}>
        Upload
      </button>
    </form>
  );
}

Common Patterns

1. Reset Form

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

  function handleReset() {
    setFormData(initialState);
  }

  return (
    <form>
      {/* inputs */}
      <button type="button" onClick={handleReset}>Reset</button>
      <button type="submit">Submit</button>
    </form>
  );
}

2. Conditional Fields

function ConditionalForm() {
  const [userType, setUserType] = useState('individual');

  return (
    <form>
      <select value={userType} onChange={(e) => setUserType(e.target.value)}>
        <option value="individual">Individual</option>
        <option value="business">Business</option>
      </select>

      {userType === 'individual' && (
        <input placeholder="First Name" />
      )}

      {userType === 'business' && (
        <>
          <input placeholder="Company Name" />
          <input placeholder="Tax ID" />
        </>
      )}
    </form>
  );
}

3. Multi-Step Form

function MultiStepForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    // step 1
    name: '',
    email: '',
    // step 2
    address: '',
    city: '',
    // step 3
    cardNumber: ''
  });

  function handleNext() {
    setStep(step + 1);
  }

  function handleBack() {
    setStep(step - 1);
  }

  function handleSubmit(event) {
    event.preventDefault();
    console.log('Final submission:', formData);
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>Step {step} of 3</p>

      {step === 1 && (
        <div>
          <h2>Personal Info</h2>
          <input
            value={formData.name}
            onChange={(e) => setFormData({ ...formData, name: e.target.value })}
            placeholder="Name"
          />
          <input
            value={formData.email}
            onChange={(e) => setFormData({ ...formData, email: e.target.value })}
            placeholder="Email"
          />
        </div>
      )}

      {step === 2 && (
        <div>
          <h2>Address</h2>
          <input
            value={formData.address}
            onChange={(e) => setFormData({ ...formData, address: e.target.value })}
            placeholder="Address"
          />
          <input
            value={formData.city}
            onChange={(e) => setFormData({ ...formData, city: e.target.value })}
            placeholder="City"
          />
        </div>
      )}

      {step === 3 && (
        <div>
          <h2>Payment</h2>
          <input
            value={formData.cardNumber}
            onChange={(e) => setFormData({ ...formData, cardNumber: e.target.value })}
            placeholder="Card Number"
          />
        </div>
      )}

      <div>
        {step > 1 && <button type="button" onClick={handleBack}>Back</button>}
        {step < 3 && <button type="button" onClick={handleNext}>Next</button>}
        {step === 3 && <button type="submit">Submit</button>}
      </div>
    </form>
  );
}

Best Practices

DO:

// Use controlled components
<input value={state} onChange={(e) => setState(e.target.value)} />

// Prevent default form submission
function handleSubmit(event) {
  event.preventDefault();
  // ...
}

// Use name attribute for multiple inputs
<input name="email" value={formData.email} onChange={handleChange} />

// Validate before submission
function handleSubmit(event) {
  event.preventDefault();
  const errors = validate();
  if (errors) return;
  // submit
}

// Disable submit button while submitting
<button disabled={isSubmitting}>Submit</button>

// Show user feedback
{submitStatus === 'success' && <p>Success!</p>}

DON’T:

// Don't forget event.preventDefault()
function handleSubmit(event) {
  // Missing preventDefault - page will reload!
  console.log('submitting');
}

// Don't use uncontrolled inputs without refs
<input />  // ❌ No value or onChange

// Don't forget to handle checkbox.checked
<input 
  type="checkbox"
  value={state}  //  Should be checked={state}
  onChange={(e) => setState(e.target.value)}  //  Should be e.target.checked
/>

// Don't mutate state directly
formData.email = 'new@email.com';  // ❌
setFormData({ ...formData, email: 'new@email.com' });  // ✅

// Don't submit without validation
function handleSubmit() {
  // ❌ No validation
  sendToAPI(formData);
}

Summary

Controlled Component Pattern:

const [value, setValue] = useState('');

<input
  value={value}
  onChange={(e) => setValue(e.target.value)}
/>

Input Types:

  • Text: value + onChange
  • Checkbox: checked + onChange with e.target.checked
  • Radio: value + checked + onChange
  • Select: value + onChange

Multiple Inputs:

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

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

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

Form Submission:

function handleSubmit(event) {
  event.preventDefault();  // Prevent page reload
  // Validate
  // Submit to API
}

<form onSubmit={handleSubmit}>

Validation:

  • Validate on submit
  • Show errors after user interaction (touched)
  • Disable submit while submitting
  • Show success/error feedback