Skip to main content

Controlled Components for React Forms Guide

Controlled components give React control over form input state, making the component's state the single source of truth. This ensures the UI and data are always in sync and enables predictable, validatable form behavior. Learn the theory, implement single and multiple inputs, and build real-world registration forms.

Key Takeaways

  • Controlled components use React state as the single source of truth for form input values
  • The value prop binds input to state, and onChange updates state with each keystroke
  • A single handleChange function can manage multiple inputs using the input's name attribute
  • Controlled components enable real-time validation, conditional rendering, and predictable form submissions

What Is a Controlled Component and Why Use It?

In standard HTML, form elements like <input>, <textarea>, and <select> maintain their own internal state. When you type, the DOM updates the input's value without JavaScript involvement—this is an uncontrolled component.

Controlled components give control of the form element's state to React. The React component's state becomes the "single source of truth" for the input's value. This means React, not the browser, determines what the input displays and what happens when the user interacts with it.

Key Principles:

  • State as the Single Source of Truth: The input's value is always driven by the component's state, never by the DOM directly.
  • State Updates via Callbacks: Changes to the input trigger callback functions (like onChange) that update state, which then updates the input's display.
  • Predictable and Explicit Data Flow: The flow is explicit: state → UI → user action → callback → setState → new UI. This makes debugging and reasoning about your application far easier.

Controlled components enable validation before updates, conditional rendering based on form state, and coordinated updates across multiple inputs. They are the modern standard in React applications.

How Do You Implement a Simple Controlled Input?

Here's a foundational example with a single text input:

import React, { useState } from 'react';

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

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

return (
<form>
<label>
Name:
<input type="text" value={name} onChange={handleChange} />
</label>
<p>Current value: {name}</p>
</form>
);
}

export default SimpleFormControl;

Step-by-Step Breakdown:

  1. const [name, setName] = useState('');: Initialize a state variable name to store the input's value. Start with an empty string.
  2. const handleChange = (event) => { setName(event.target.value); }: This event handler receives the event object, extracts the input's current value via event.target.value, and calls setName to update state.
  3. <input type="text" value={name} onChange={handleChange} />: This is the core of the controlled component:
    • value={name}: The input's displayed value is always set to the name state variable.
    • onChange={handleChange}: Every time the user types, handleChange is called, updating the state.
  4. <p>Current value: {name}</p>: Displays the state value to prove React controls the input.

When you type, the flow is: input → onChange fires → handleChange updates state → component re-renders → input's value prop reflects new state → display updates.

How Do You Handle Multiple Form Inputs?

Instead of a separate state variable and handler for each input, use a single state object and a generic handler:

import React, { useState } from 'react';

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

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

return (
<form>
<label>
First Name:
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
</label>
<br />
<label>
Last Name:
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</label>
<p>Hello, {formData.firstName} {formData.lastName}</p>
</form>
);
}

export default MultiInputForm;

How It Works:

  • Single State Object: formData holds all input values.
  • Destructuring in Handler: const { name, value } = event.target; extracts the input's name and the new value.
  • Computed Property Key: [name]: value uses the input's name attribute dynamically, so one handler works for all inputs.
  • Immutable Update: { ...prevFormData, [name]: value } creates a new object, copying old values and overriding the changed field.
  • Dynamic UI: As the user types, the greeting updates in real time.

This pattern scales: add more inputs without adding more handlers—just ensure each has a unique name attribute and a corresponding state key.

Building a Real-World Registration Form

Let's apply this to a practical form with email, password, and form submission:

import React, { useState } from 'react';

function RegistrationForm() {
const [formState, setFormState] = useState({
email: '',
password: '',
});

const handleChange = (event) => {
const { name, value } = event.target;
setFormState(prevState => ({
...prevState,
[name]: value,
}));
};

const handleSubmit = (event) => {
event.preventDefault();
alert(`Form submitted with Email: ${formState.email} and Password: ${formState.password}`);
};

return (
<form onSubmit={handleSubmit}>
<h2>Register</h2>
<label>
Email:
<input
type="email"
name="email"
value={formState.email}
onChange={handleChange}
/>
</label>
<br />
<label>
Password:
<input
type="password"
name="password"
value={formState.password}
onChange={handleChange}
/>
</label>
<br />
<button type="submit">Register</button>
</form>
);
}

export default RegistrationForm;

Key Points:

  • onSubmit={handleSubmit}: The form's submit handler is called when the user clicks "Register" or presses Enter.
  • event.preventDefault();: Prevents the form from reloading the page (the default browser behavior).
  • State at Submit Time: formState contains the current email and password when submitted. You can send this to a server, validate it, or store it.

This form demonstrates the full lifecycle: user input → state updates → form submission with validated data.

How Controlled Components Work: The Cycle

When a user types into a controlled input:

  1. User types: The input element detects the keystroke.
  2. onChange fires: The browser raises the onChange event.
  3. Handler called: Your handleChange function is invoked with the event object.
  4. State updated: setFormState or setName is called with the new value.
  5. Re-render triggered: React schedules a re-render of the component.
  6. Input re-evaluated: The component renders, and the input's value prop is set to the new state value.
  7. UI updates: The user sees the new character in the input.

This cycle ensures the input's displayed value always reflects the component's state—they're synchronized.

Best Practices for Controlled Components

Do:

  • Always initialize state: Start with a default value (usually '') to avoid uncontrolled inputs.
  • Use a single handler for multiple inputs: This keeps code DRY.
  • Maintain immutability: Use the spread operator when updating state objects.
  • Validate on change: Update error states as the user types for real-time feedback.

Avoid:

  • Passing null or undefined as the value: This makes the input uncontrolled. Always use '' or a meaningful default.
  • Mixing controlled and uncontrolled: A single input should use either value (controlled) or not, never both.
  • Direct DOM manipulation: Never use refs to directly read or set an input's value if it's controlled by React.
  • Expensive operations in onChange: For inputs triggering API calls, debounce the handler to avoid excessive requests.

Frequently Asked Questions

What is the difference between controlled and uncontrolled components?

A controlled component has its value managed by React state. The input's value prop is bound to state, and onChange updates state. A uncontrolled component manages its own state—you don't set a value prop, and you read the input's value via a ref when needed. Controlled components are predictable and recommended for most use cases.

Can I use a controlled component with a default value?

Yes. Initialize state with the default value:

const [name, setName] = useState('John Doe');

The input will start displaying 'John Doe'. If you change state later, the input updates immediately.

Why does my controlled input feel slow?

This is rare but can happen with very complex forms or expensive render logic. For most applications, re-rendering on every keystroke is not a performance issue. If needed, debounce the onChange handler using libraries like lodash.debounce or custom hooks.

How do I validate a controlled form input?

Update state for both the value and any errors:

const handleChange = (event) => {
const value = event.target.value;
setFormData({ ...formData, email: value });

if (!value.includes('@')) {
setError('Email must contain @');
} else {
setError('');
}
};

Then display the error conditionally: {error && <p style={{color: 'red'}}>{error}</p>}.

Can I disable form submission until the form is valid?

Yes:

<button type="submit" disabled={formState.email === '' || formState.password === ''}>
Register
</button>

The button is disabled until both fields have values.

Conclusion

Controlled components are the foundation of predictable, validatable forms in React. By treating the component's state as the single source of truth and using onChange to update it, you ensure the UI and data are always in sync. This pattern scales from simple single-input forms to complex multi-step registration wizards.

Challenge Yourself: Add a "confirm password" field to the registration form. Disable the submit button until both passwords match, and display an error message if they don't.


Glossary

  • Controlled Component: A form element whose value is managed by React state through the value prop and updated via the onChange handler.
  • Uncontrolled Component: A form element that manages its own state internally without React intervention.
  • Single Source of Truth: The principle that a component's state is the authoritative source for its UI, ensuring consistency.
  • Event Handler: A function called in response to a user action (like typing, clicking, or submitting).

Further Reading