Master form handling in React with controlled components, learn to manage input state, handle submissions, and build interactive forms.
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:
function NameForm() {
const [name, setName] = React.useState('');
return (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}With this pattern:
onChange event.setName() with the new value.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:
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.
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):
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:
function handleSubmit(e) {
e.preventDefault();
// Process the data...
setEmail('');
setPassword('');
}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:
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:
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:
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>
);
}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:
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:
function handleChange(e) {
const { name, value, type, checked } = e.target;
setForm((prev) => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
}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:
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):
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:
<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.
<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?