Controlled Forms
In React, form inputs are typically controlled — their value is driven by React state, not the DOM. This means React is always the single source of truth for what's in the input.
The Controlled Input Pattern
controlled.jsx
import { useState } from 'react';
function ControlledInput() {
const [value, setValue] = useState('');
return (
<div>
{/* React controls the input value */}
<input
type="text"
value={value} // driven by state
onChange={e => setValue(e.target.value)} // update state on change
placeholder="Type here..."
/>
<p>You typed: "{value}"</p>
<p>Length: {value.length}</p>
<button onClick={() => setValue('')}>Clear</button>
</div>
);
}
// The input is "controlled" because React owns its value.
// The DOM is just a display layer.
A Complete Form
The pattern scales to any number of fields:
full-form.jsx
import { useState } from 'react';
function SignupForm() {
const [form, setForm] = useState({
name: '', email: '', password: '', role: 'student'
});
// Generic handler works for all text fields
function handleChange(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
console.log('Submitting:', form);
// Send to API...
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange}
placeholder="Full name" required />
<input name="email" type="email" value={form.email} onChange={handleChange}
placeholder="Email" required />
<input name="password" type="password" value={form.password} onChange={handleChange}
placeholder="Password" minLength={8} required />
<select name="role" value={form.role} onChange={handleChange}>
<option value="student">Student</option>
<option value="teacher">Teacher</option>
</select>
<button type="submit">Sign Up</button>
</form>
);
}
Validation
validation.jsx
import { useState } from 'react';
function ValidatedForm() {
const [email, setEmail] = useState('');
const [errors, setErrors] = useState({});
function validate() {
const newErrors = {};
if (!email) newErrors.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(email)) newErrors.email = 'Invalid email format';
return newErrors;
}
function handleSubmit(e) {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
setErrors({});
alert('Success! Email: ' + email);
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={email}
onChange={e => { setEmail(e.target.value); setErrors({}); }}
style={{ borderColor: errors.email ? 'red' : undefined }}
placeholder="your@email.com"
/>
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
}
Select, Checkbox, Radio
other-inputs.jsx
import { useState } from 'react';
function AllInputTypes() {
const [color, setColor] = useState('blue');
const [agreed, setAgreed] = useState(false);
const [size, setSize] = useState('medium');
return (
<form>
{/* Select */}
<select value={color} onChange={e => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
<p>Selected: {color}</p>
{/* Checkbox */}
<label>
<input type="checkbox" checked={agreed}
onChange={e => setAgreed(e.target.checked)} />
I agree to the terms
</label>
<p>Agreed: {agreed ? 'Yes' : 'No'}</p>
{/* Radio buttons */}
{['small', 'medium', 'large'].map(s => (
<label key={s}>
<input type="radio" value={s} checked={size === s}
onChange={e => setSize(e.target.value)} />
{s}
</label>
))}
<p>Size: {size}</p>
</form>
);
}