React / React Forms / Controlled Components
Home /React /React Forms /Controlled Components

Controlled Components

Controlled Components

◆ Definition

Controlled Component: An input element whose value is driven by React state at all times. The component re-renders on every change, and the displayed value always reflects state React is the single source of truth.

The pattern always requires two things: a value prop pointing to state, and an onChange handler that updates that state on every keystroke.

Basic Controlled Input

JSX — BasicInput.jsx

import { useState } from 'react';

function BasicInput() {
  // Step 1: Create state to hold the input's value
  const [name, setName] = useState('');

  // Step 2: Handle changes — update state on every keystroke
  const handleChange = (e) => {
    setName(e.target.value);
  };

  return (
    <div>
      <label htmlFor="name">Full Name</label>
      <input
        id="name"
        type="text"
        value={name}              // ← React controls the displayed value
        onChange={handleChange}  // ← React updates on every keystroke
        placeholder="Enter your name"
      />
      <p>Live preview: {name || 'stranger'}</p>
    </div>
  );
}

export default BasicInput;

Complete Login Form

A real-world controlled form with two fields, a submit handler, and loading state:

JSX — LoginForm.jsx

import { useState } from 'react';

function LoginForm() {
  const [email, setEmail]       = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault(); // ← ALWAYS prevent default page reload first
    setIsLoading(true);
    try {
      await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="you@example.com"
        required
      />

      <label htmlFor="password">Password</label>
      <input
        id="password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        required
      />

      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Log In'}
      </button>
    </form>
  );
}

export default LoginForm;

Always put e.preventDefault() as the very first line of your onSubmit handler. If an error is thrown before it reaches that line, the browser will still submit and reload the page.