Forms & Controlled Components
Forms are fundamental to web applications. React provides two approaches: Controlled Components (React manages state) and Uncontrolled Components (DOM manages state).Controlled vs Uncontrolled
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| State location | React state | DOM |
| Access to value | useState variable | useRef |
| Validation | On every keystroke | On submit |
| Use case | Most forms | File inputs, quick prototypes |
Copy
Controlled Component:
┌─────────────────────────────────────────────────────────┐
│ User types → onChange → setState → re-render │
│ ↓ │
│ value={state} │
│ ↓ │
│ Input displays state value │
└─────────────────────────────────────────────────────────┘
Uncontrolled Component:
┌─────────────────────────────────────────────────────────┐
│ User types → DOM handles value │
│ ↓ │
│ On submit → ref.current.value │
└─────────────────────────────────────────────────────────┘
Controlled Components
With controlled components, React state is the “single source of truth” for input values.Basic Text Input
Copy
import { useState } from 'react';
function TextInput() {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Type something..."
/>
<p>You typed: {value}</p>
</div>
);
}
Complete Form Example
Copy
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
agreeToTerms: false,
role: 'user',
interests: []
});
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleInterestChange = (event) => {
const { value, checked } = event.target;
setFormData(prev => ({
...prev,
interests: checked
? [...prev.interests, value]
: prev.interests.filter(i => i !== value)
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
{/* Text Input */}
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
required
/>
</div>
{/* Email Input */}
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
{/* Password Inputs */}
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
minLength={8}
required
/>
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
/>
</div>
{/* Select Dropdown */}
<div>
<label htmlFor="role">Role:</label>
<select
id="role"
name="role"
value={formData.role}
onChange={handleChange}
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="moderator">Moderator</option>
</select>
</div>
{/* Checkbox Group */}
<fieldset>
<legend>Interests:</legend>
{['Technology', 'Sports', 'Music', 'Art'].map(interest => (
<label key={interest}>
<input
type="checkbox"
value={interest}
checked={formData.interests.includes(interest)}
onChange={handleInterestChange}
/>
{interest}
</label>
))}
</fieldset>
{/* Single Checkbox */}
<div>
<label>
<input
type="checkbox"
name="agreeToTerms"
checked={formData.agreeToTerms}
onChange={handleChange}
required
/>
I agree to the terms and conditions
</label>
</div>
<button type="submit">Register</button>
</form>
);
}
Form Elements Reference
Textarea
In React,<textarea> uses a value prop instead of children:
Copy
// ❌ HTML way
<textarea>Initial text</textarea>
// ✅ React way
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
/>
Select
Usevalue on the <select> element, not selected on options:
Copy
// ❌ HTML way
<select>
<option selected>Apple</option>
<option>Banana</option>
</select>
// ✅ React way
<select value={fruit} onChange={(e) => setFruit(e.target.value)}>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</select>
Multiple Select
Copy
function MultiSelect() {
const [selected, setSelected] = useState([]);
const handleChange = (e) => {
const options = Array.from(e.target.selectedOptions);
setSelected(options.map(option => option.value));
};
return (
<select multiple value={selected} onChange={handleChange}>
<option value="react">React</option>
<option value="vue">Vue</option>
<option value="angular">Angular</option>
</select>
);
}
Radio Buttons
Copy
function RadioGroup() {
const [selected, setSelected] = useState('medium');
return (
<fieldset>
<legend>Size:</legend>
{['small', 'medium', 'large'].map(size => (
<label key={size}>
<input
type="radio"
name="size"
value={size}
checked={selected === size}
onChange={(e) => setSelected(e.target.value)}
/>
{size.charAt(0).toUpperCase() + size.slice(1)}
</label>
))}
</fieldset>
);
}
Form Validation
Real-Time Validation
Copy
function ValidatedForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (name, value) => {
switch (name) {
case 'email':
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format';
return '';
case 'password':
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!/[A-Z]/.test(value)) return 'Password must contain uppercase letter';
if (!/[0-9]/.test(value)) return 'Password must contain a number';
return '';
default:
return '';
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Validate on change if field has been touched
if (touched[name]) {
setErrors(prev => ({ ...prev, [name]: validate(name, value) }));
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
setErrors(prev => ({ ...prev, [name]: validate(name, value) }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Validate all fields
const newErrors = {};
Object.keys(formData).forEach(key => {
const error = validate(key, formData[key]);
if (error) newErrors[key] = error;
});
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
setTouched({ email: true, password: true });
return;
}
console.log('Form is valid:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={errors.email && touched.email ? 'error' : ''}
/>
{errors.email && touched.email && (
<span className="error-message">{errors.email}</span>
)}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={errors.password && touched.password ? 'error' : ''}
/>
{errors.password && touched.password && (
<span className="error-message">{errors.password}</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
Password Strength Indicator
Copy
function PasswordStrength({ password }) {
const getStrength = (pwd) => {
let strength = 0;
if (pwd.length >= 8) strength++;
if (pwd.length >= 12) strength++;
if (/[A-Z]/.test(pwd)) strength++;
if (/[a-z]/.test(pwd)) strength++;
if (/[0-9]/.test(pwd)) strength++;
if (/[^A-Za-z0-9]/.test(pwd)) strength++;
return strength;
};
const strength = getStrength(password);
const labels = ['', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong', 'Excellent'];
const colors = ['', '#ef4444', '#f97316', '#eab308', '#22c55e', '#16a34a', '#15803d'];
return (
<div className="password-strength">
<div className="strength-bars">
{[1, 2, 3, 4, 5, 6].map(level => (
<div
key={level}
className="strength-bar"
style={{
backgroundColor: strength >= level ? colors[strength] : '#e5e7eb',
height: '4px',
flex: 1,
marginRight: '2px',
borderRadius: '2px'
}}
/>
))}
</div>
<span style={{ color: colors[strength] }}>{labels[strength]}</span>
</div>
);
}
Uncontrolled Components with useRef
For simple forms or when you need direct DOM access:Copy
import { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef();
const emailRef = useRef();
const fileRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
console.log({
name: nameRef.current.value,
email: emailRef.current.value,
file: fileRef.current.files[0]
});
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="John" />
<input ref={emailRef} type="email" />
<input ref={fileRef} type="file" />
<button type="submit">Submit</button>
</form>
);
}
File inputs are always uncontrolled in React because their value can only be set by the user, not programmatically.
Copy
// File input example
function FileUpload() {
const [file, setFile] = useState(null);
const handleChange = (e) => {
const selectedFile = e.target.files[0];
setFile(selectedFile);
};
return (
<div>
<input type="file" onChange={handleChange} accept="image/*" />
{file && <p>Selected: {file.name}</p>}
</div>
);
}
Form Libraries
For complex forms, consider using a form library:React Hook Form
Copy
npm install react-hook-form
Copy
import { useForm } from 'react-hook-form';
function HookFormExample() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm();
const onSubmit = async (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^\S+@\S+$/i,
message: 'Invalid email format'
}
})}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input
type="password"
{...register('password', {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters'
}
})}
placeholder="Password"
/>
{errors.password && <span>{errors.password.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
React Hook Form advantages:
- Less re-renders (uses refs under the hood)
- Built-in validation
- Easy integration with UI libraries
- Small bundle size
Advanced Patterns
Debounced Input
Copy
import { useState, useEffect, useCallback } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// Make API call
console.log('Searching for:', debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Form with Dynamic Fields
Copy
function DynamicForm() {
const [fields, setFields] = useState([{ id: 1, value: '' }]);
const addField = () => {
setFields([...fields, { id: Date.now(), value: '' }]);
};
const removeField = (id) => {
if (fields.length > 1) {
setFields(fields.filter(f => f.id !== id));
}
};
const updateField = (id, value) => {
setFields(fields.map(f => f.id === id ? { ...f, value } : f));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Values:', fields.map(f => f.value));
};
return (
<form onSubmit={handleSubmit}>
{fields.map((field, index) => (
<div key={field.id} className="field-row">
<input
value={field.value}
onChange={(e) => updateField(field.id, e.target.value)}
placeholder={`Field ${index + 1}`}
/>
<button
type="button"
onClick={() => removeField(field.id)}
disabled={fields.length === 1}
>
Remove
</button>
</div>
))}
<button type="button" onClick={addField}>Add Field</button>
<button type="submit">Submit</button>
</form>
);
}
🎯 Practice Exercises
Exercise 1: Credit Card Form
Exercise 1: Credit Card Form
Copy
function CreditCardForm() {
const [card, setCard] = useState({
number: '',
name: '',
expiry: '',
cvv: ''
});
const formatCardNumber = (value) => {
return value
.replace(/\D/g, '')
.slice(0, 16)
.replace(/(.{4})/g, '$1 ')
.trim();
};
const formatExpiry = (value) => {
return value
.replace(/\D/g, '')
.slice(0, 4)
.replace(/^(.{2})/, '$1/');
};
const handleChange = (e) => {
const { name, value } = e.target;
let formatted = value;
if (name === 'number') formatted = formatCardNumber(value);
if (name === 'expiry') formatted = formatExpiry(value);
if (name === 'cvv') formatted = value.replace(/\D/g, '').slice(0, 3);
setCard(prev => ({ ...prev, [name]: formatted }));
};
return (
<form onSubmit={(e) => { e.preventDefault(); console.log(card); }}>
<input
name="number"
value={card.number}
onChange={handleChange}
placeholder="1234 5678 9012 3456"
maxLength={19}
/>
<input
name="name"
value={card.name}
onChange={(e) => setCard(prev => ({ ...prev, name: e.target.value.toUpperCase() }))}
placeholder="CARDHOLDER NAME"
/>
<input
name="expiry"
value={card.expiry}
onChange={handleChange}
placeholder="MM/YY"
maxLength={5}
/>
<input
name="cvv"
value={card.cvv}
onChange={handleChange}
placeholder="CVV"
maxLength={3}
type="password"
/>
<button type="submit">Pay</button>
</form>
);
}
Exercise 2: Multi-Step Form
Exercise 2: Multi-Step Form
Copy
function MultiStepForm() {
const [step, setStep] = useState(1);
const [data, setData] = useState({
// Step 1
firstName: '',
lastName: '',
// Step 2
email: '',
phone: '',
// Step 3
plan: 'basic',
addons: []
});
const updateData = (field, value) => {
setData(prev => ({ ...prev, [field]: value }));
};
const next = () => setStep(s => Math.min(s + 1, 4));
const prev = () => setStep(s => Math.max(s - 1, 1));
return (
<div className="multi-step">
<div className="steps-indicator">
{[1, 2, 3, 4].map(s => (
<div
key={s}
className={`step ${step >= s ? 'active' : ''}`}
>
{s}
</div>
))}
</div>
{step === 1 && (
<div className="step-content">
<h2>Personal Info</h2>
<input
value={data.firstName}
onChange={(e) => updateData('firstName', e.target.value)}
placeholder="First Name"
/>
<input
value={data.lastName}
onChange={(e) => updateData('lastName', e.target.value)}
placeholder="Last Name"
/>
</div>
)}
{step === 2 && (
<div className="step-content">
<h2>Contact Info</h2>
<input
type="email"
value={data.email}
onChange={(e) => updateData('email', e.target.value)}
placeholder="Email"
/>
<input
type="tel"
value={data.phone}
onChange={(e) => updateData('phone', e.target.value)}
placeholder="Phone"
/>
</div>
)}
{step === 3 && (
<div className="step-content">
<h2>Select Plan</h2>
{['basic', 'pro', 'enterprise'].map(plan => (
<label key={plan}>
<input
type="radio"
name="plan"
value={plan}
checked={data.plan === plan}
onChange={(e) => updateData('plan', e.target.value)}
/>
{plan.charAt(0).toUpperCase() + plan.slice(1)}
</label>
))}
</div>
)}
{step === 4 && (
<div className="step-content">
<h2>Confirm</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
<div className="buttons">
{step > 1 && <button onClick={prev}>Back</button>}
{step < 4 ? (
<button onClick={next}>Next</button>
) : (
<button onClick={() => alert('Submitted!')}>Submit</button>
)}
</div>
</div>
);
}
Summary
| Concept | Description |
|---|---|
| Controlled Components | React state manages form input values |
| Uncontrolled Components | DOM manages values, accessed via refs |
| value + onChange | Required pattern for controlled inputs |
| event.target | Access input name, value, type, checked |
| Real-time Validation | Validate on change or blur |
| touched state | Track if user has interacted with field |
| useRef | Access DOM elements directly |
| React Hook Form | Popular library for complex forms |
| Debouncing | Delay processing for search inputs |
Next Steps
In the next chapter, you’ll learn about useEffect & Side Effects — fetching data, subscriptions, and more!