Beginner25 min read

Forms & Controlled Components

Master form handling in React with controlled components, learn to manage input state, handle submissions, and build interactive forms.

Controlled vs. Uncontrolled Inputs

In HTML, form elements like <input>, <textarea>, and <select> maintain their own internal state — the browser keeps track of what the user has typed. In React, we typically want React state to be the single source of truth for form data. This is called a controlled component.

A controlled input is one whose value is driven by React state. You set the input's value attribute to a state variable and update that state via an onChange handler:

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

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

With this pattern:

  1. The user types a character.
  2. The browser fires the onChange event.
  3. Your handler calls setName() with the new value.
  4. React re-renders the component with the updated state.
  5. The input displays the new value from state.

This may seem like extra work, but it gives you complete control over the form data. You can validate input as the user types, transform values (e.g., force uppercase), conditionally prevent changes, or synchronize multiple inputs.

An uncontrolled component is one where the DOM itself holds the state. You access the value using a ref instead of tracking it in state:

jsx
function UncontrolledForm() {
  const inputRef = React.useRef(null);

  function handleSubmit() {
    alert(inputRef.current.value);
  }

  return <input ref={inputRef} type="text" />;
}

Uncontrolled components are simpler for basic cases, but controlled components are recommended for most React forms because they keep the data flow predictable.

Handling Form Submission

To handle form submission in React, attach an onSubmit handler to the <form> element. Always call e.preventDefault() to stop the browser from refreshing the page (the default HTML form behavior):

jsx
function LoginForm() {
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');

  function handleSubmit(e) {
    e.preventDefault();
    console.log('Submitting:', { email, password });
    // Send data to an API, validate, etc.
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email:
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <label>
        Password:
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </label>
      <button type="submit">Log In</button>
    </form>
  );
}

The onSubmit handler fires when the user clicks the submit button or presses Enter while focused on an input inside the form. Using onSubmit on the form (rather than onClick on the button) is important because it catches both submission methods.

After submission, you can clear the form by resetting the state:

jsx
function handleSubmit(e) {
  e.preventDefault();
  // Process the data...
  setEmail('');
  setPassword('');
}

Textarea and Select

In HTML, <textarea> uses its children for the value, and <select> uses the selected attribute on <option>. In React, both are controlled the same way as <input> — using value and onChange.

Textarea:

jsx
function CommentBox() {
  const [comment, setComment] = React.useState('');

  return (
    <textarea
      value={comment}
      onChange={(e) => setComment(e.target.value)}
      rows={4}
      placeholder="Write a comment..."
    />
  );
}

In React, <textarea> uses the value prop instead of children. This makes it consistent with <input>.

Select:

jsx
function ColorPicker() {
  const [color, setColor] = React.useState('green');

  return (
    <select value={color} onChange={(e) => setColor(e.target.value)}>
      <option value="red">Red</option>
      <option value="green">Green</option>
      <option value="blue">Blue</option>
    </select>
  );
}

The value on <select> determines which option is selected. You do not need to put a selected attribute on individual <option> elements — React handles it through the value prop.

Multiple select:

jsx
function MultiSelect() {
  const [selected, setSelected] = React.useState([]);

  function handleChange(e) {
    const options = Array.from(e.target.selectedOptions);
    setSelected(options.map((o) => o.value));
  }

  return (
    <select multiple value={selected} onChange={handleChange}>
      <option value="js">JavaScript</option>
      <option value="py">Python</option>
      <option value="go">Go</option>
    </select>
  );
}

Multiple Inputs with a Single Handler

When your form has many inputs, creating a separate state variable and handler for each one can be tedious. A common pattern is to use a single state object and a single handler that uses the input's name attribute to determine which field to update:

jsx
function RegistrationForm() {
  const [form, setForm] = React.useState({
    username: '',
    email: '',
    age: '',
  });

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

  function handleSubmit(e) {
    e.preventDefault();
    console.log(form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={form.username}
        onChange={handleChange}
        placeholder="Username"
      />
      <input
        name="email"
        type="email"
        value={form.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="age"
        type="number"
        value={form.age}
        onChange={handleChange}
        placeholder="Age"
      />
      <button type="submit">Register</button>
    </form>
  );
}

The key technique is computed property names: [name]: value. This dynamically sets the correct property in the state object based on which input triggered the change event.

For checkboxes, use e.target.checked instead of e.target.value:

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

Form Validation Basics

Form validation ensures that users provide valid data before submission. You can validate on submit, on change (as the user types), or on blur (when the user leaves an input).

Validation on submit:

jsx
function SignupForm() {
  const [email, setEmail] = React.useState('');
  const [error, setError] = React.useState('');

  function handleSubmit(e) {
    e.preventDefault();
    if (!email.includes('@')) {
      setError('Please enter a valid email address');
      return;
    }
    setError('');
    console.log('Submitted:', email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button type="submit">Sign Up</button>
    </form>
  );
}

Validation on change (real-time feedback):

jsx
function PasswordInput() {
  const [password, setPassword] = React.useState('');

  const isValid = password.length >= 8;

  return (
    <div>
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <p style={{ color: isValid ? 'green' : 'red' }}>
        {isValid ? 'Password is strong enough' : 'Password must be at least 8 characters'}
      </p>
    </div>
  );
}

Disabling the submit button until the form is valid is a common UX pattern:

jsx
<button type="submit" disabled={!email || !password}>
  Submit
</button>

For production apps, consider using a form library like React Hook Form or Formik for complex validation, but understanding the fundamentals of controlled components is essential before reaching for those tools.

Forms in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  const { useState } = React;

  function App() {
    const [input, setInput] = useState('');
    const [items, setItems] = useState(['Learn React', 'Practice forms']);

    function handleSubmit(e) {
      e.preventDefault();
      if (input.trim()) {
        setItems([...items, input.trim()]);
        setInput('');
      }
    }

    return (
      <div>
        <h2>Todo List</h2>
        <form onSubmit={handleSubmit}>
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Add a task..."
          />
          <button type="submit">Add</button>
        </form>
        <ul>
          {items.map((item, i) => (
            <li key={i}>{item}</li>
          ))}
        </ul>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

In a controlled input, what determines the displayed value?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.