10. Forms & Inputs

Chapter 10: Forms & Controlled Components

Forms are the primary medium for gathering user data. In standard HTML, form elements maintain their own internal state. In React, we establish a single source of truth by binding DOM elements to React state. Learn how to build rock-solid controlled forms, handle complex multi-field inputs, and apply validation.

1. Controlled vs. Uncontrolled Components

The distinction between controlled and uncontrolled inputs is fundamental to frontend architecture:

CriteriaControlled Components (Standard Practice)Uncontrolled Components (Alternative)
State LocationReact component state (useState).Browser DOM node internally.
Value BindingControlled via value={state} and onChange.Read on-demand via useRef() or FormData.
Re-render BehaviorRe-renders component on every keystroke.Zero re-renders during typing; value read on submit.
Best ForInstant validation, masked inputs, disabling submit dynamically.File uploads, huge forms, non-validated rapid inputs.

Basic Controlled Input Example:

import { useState } from 'react';

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

  return (
    <div>
      <label htmlFor="emailInput">Email Address:</label>
      <input
        id="emailInput"
        type="email"
        value={email} // Value strictly driven by React state
        onChange={(e) => setEmail(e.target.value)} // State updated on every keystroke
      />
      <p>Typed Email: {email}</p>
    </div>
  );
}

2. Handling Multiple Inputs with a Single State Object

Creating a separate useState hook for 10 different form fields leads to unnecessary boilerplate. Instead, store form data inside a single object and use computed property names matching the input's name attribute:

import { useState } from 'react';

function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    role: 'developer',
    termsAccepted: false
  });

  function handleChange(event) {
    const { name, value, type, checked } = event.target;

    setFormData(prev => ({
      ...prev,
      // Checkboxes use the 'checked' boolean, while text inputs use 'value'
      [name]: type === 'checkbox' ? checked : value
    }));
  }

  function handleSubmit(event) {
    event.preventDefault();
    console.log("Submitting payload to API:", formData);
  }

  return (
    <form onSubmit={handleSubmit} className="p-3 border rounded">
      <div className="mb-3">
        <label>Username</label>
        <input 
          type="text" 
          name="username" 
          value={formData.username} 
          onChange={handleChange} 
        />
      </div>

      <div className="mb-3">
        <label>Role</label>
        <select name="role" value={formData.role} onChange={handleChange}>
          <option value="developer">Software Developer</option>
          <option value="designer">UI/UX Designer</option>
          <option value="manager">Product Manager</option>
        </select>
      </div>

      <div className="mb-3">
        <label>
          <input 
            type="checkbox" 
            name="termsAccepted" 
            checked={formData.termsAccepted} 
            onChange={handleChange} 
          />
          I accept terms and conditions
        </label>
      </div>

      <button type="submit" disabled={!formData.termsAccepted}>
        Register Account
      </button>
    </form>
  );
}

3. Uncontrolled Components & File Uploads

In an uncontrolled component, you query the DOM element directly when the form is submitted using useRef(). This is the mandatory pattern when handling file inputs (<input type="file" />):

import { useRef } from 'react';

function AvatarUploadForm() {
  const fileInputRef = useRef(null);
  const notesInputRef = useRef(null);

  function handleUpload(e) {
    e.preventDefault();
    
    // Access the raw DOM elements directly
    const selectedFile = fileInputRef.current.files[0];
    const notes = notesInputRef.current.value;

    if (!selectedFile) {
      alert("Please select an image file first.");
      return;
    }

    const payload = new FormData();
    payload.append("avatar", selectedFile);
    payload.append("notes", notes);

    console.log("Uploading file to cloud server...");
  }

  return (
    <form onSubmit={handleUpload}>
      <input type="file" ref={fileInputRef} accept="image/*" />
      <input type="text" ref={notesInputRef} placeholder="Avatar notes..." />
      <button type="submit">Upload Photo</button>
    </form>
  );
}

4. Form Validation Patterns & Best Practices

Robust form UX relies on clear, immediate feedback. Professional teams implement a three-tiered validation strategy:

1. Inline Client Validation

Validate email formats, password strength, and required fields before submitting. Display helpful red error labels underneath the specific invalid field.

2. Disable Submit When Invalid

Keep the submission button disabled until all mandatory fields pass validation tests, preventing needless error roundtrips.

3. Server-Side Validation

Client validation is purely for user convenience. Never trust client data! Always re-validate all payloads on your backend API to prevent injection and tampering.

Frequently Asked Questions (FAQ)

What is the difference between a Controlled and an Uncontrolled component in React?

In a Controlled component, the form element's value is driven by React state (value={state}), and user updates trigger an onChange handler that updates that state; React is the single source of truth. In an Uncontrolled component, the DOM itself maintains the input's state, and you pull the value on demand when needed using a React ref (useRef).

Why is an <input type='file' /> always an uncontrolled component in React?

For security reasons, web browsers prevent programmatic manipulation of file system paths. The value of a file input is strictly read-only and can only be set directly by user interaction with the browser's native file picker. Therefore, it is always uncontrolled in React and accessed via a ref or e.target.files.

How do you handle multiple input fields with a single change handler in React?

Give each input a 'name' attribute matching the corresponding key in your state object. In the change handler, use computed property names: setFormData(prev => ({ ...prev, [e.target.name]: e.target.value })). For checkboxes, check e.target.type === 'checkbox' and read e.target.checked.

When should you adopt form libraries like React Hook Form instead of native useState?

For small forms (1-4 fields), native useState is simple and effective. For complex forms with dozens of fields, complex interdependent validation, or high-frequency input, React Hook Form is recommended because it uses uncontrolled inputs under the hood to isolate re-renders, dramatically improving performance.

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools