Controlled vs Uncontrolled Components
In React, form inputs can be controlled (React manages the value) or uncontrolled (DOM manages the value). Think of controlled components as having React as the puppet master, while uncontrolled components are free spirits.
Controlled Components
Basic Controlled Input
function ControlledInput() {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Type something..."
/>
<p>Current value: {value}</p>
</div>
);
}
Multiple Inputs
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
/>
</div>
<button type="submit">Register</button>
</form>
);
}
Different Input Types
Textarea
function TextareaExample() {
const [comment, setComment] = useState('');
return (
<div>
<label htmlFor="comment">Comment:</label>
<textarea
id="comment"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={4}
cols={50}
/>
<p>Character count: {comment.length}</p>
</div>
);
}
Select Dropdown
function SelectExample() {
const [selectedFruit, setSelectedFruit] = useState('');
return (
<div>
<label htmlFor="fruit">Choose a fruit:</label>
<select
id="fruit"
value={selectedFruit}
onChange={(e) => setSelectedFruit(e.target.value)}
>
<option value="">Select a fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="orange">Orange</option>
</select>
<p>Selected: {selectedFruit}</p>
</div>
);
}
Multiple Select
function MultiSelectExample() {
const [selectedToppings, setSelectedToppings] = useState([]);
const handleChange = (e) => {
const options = [...e.target.selectedOptions];
const values = options.map(option => option.value);
setSelectedToppings(values);
};
return (
<div>
<label htmlFor="toppings">Choose pizza toppings:</label>
<select
id="toppings"
multiple
value={selectedToppings}
onChange={handleChange}
>
<option value="cheese">Cheese</option>
<option value="pepperoni">Pepperoni</option>
<option value="mushrooms">Mushrooms</option>
<option value="olives">Olives</option>
</select>
<p>Selected: {selectedToppings.join(', ')}</p>
</div>
);
}
Checkboxes
function CheckboxExample() {
const [preferences, setPreferences] = useState({
newsletter: false,
notifications: false,
marketing: false
});
const handleCheckboxChange = (e) => {
const { name, checked } = e.target;
setPreferences(prev => ({
...prev,
[name]: checked
}));
};
return (
<div>
<h3>Email Preferences</h3>
<label>
<input
type="checkbox"
name="newsletter"
checked={preferences.newsletter}
onChange={handleCheckboxChange}
/>
Newsletter
</label>
<label>
<input
type="checkbox"
name="notifications"
checked={preferences.notifications}
onChange={handleCheckboxChange}
/>
Notifications
</label>
<label>
<input
type="checkbox"
name="marketing"
checked={preferences.marketing}
onChange={handleCheckboxChange}
/>
Marketing emails
</label>
<pre>{JSON.stringify(preferences, null, 2)}</pre>
</div>
);
}
Radio Buttons
function RadioExample() {
const [gender, setGender] = useState('');
return (
<div>
<h3>Select Gender</h3>
<label>
<input
type="radio"
value="male"
checked={gender === 'male'}
onChange={(e) => setGender(e.target.value)}
/>
Male
</label>
<label>
<input
type="radio"
value="female"
checked={gender === 'female'}
onChange={(e) => setGender(e.target.value)}
/>
Female
</label>
<label>
<input
type="radio"
value="other"
checked={gender === 'other'}
onChange={(e) => setGender(e.target.value)}
/>
Other
</label>
<p>Selected: {gender}</p>
</div>
);
}
File Inputs
function FileUpload() {
const [selectedFile, setSelectedFile] = useState(null);
const [preview, setPreview] = useState('');
const handleFileChange = (e) => {
const file = e.target.files[0];
setSelectedFile(file);
// Create preview for images
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(file);
} else {
setPreview('');
}
};
return (
<div>
<input
type="file"
onChange={handleFileChange}
accept="image/*"
/>
{selectedFile && (
<div>
<p>File name: {selectedFile.name}</p>
<p>File size: {(selectedFile.size / 1024).toFixed(2)} KB</p>
<p>File type: {selectedFile.type}</p>
</div>
)}
{preview && (
<img
src={preview}
alt="Preview"
style={{ maxWidth: '200px' }}
/>
)}
</div>
);
}
Form Validation
Basic Validation
function ValidatedForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
// Email validation
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
// Password validation
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
console.log('Form submitted:', formData);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
type="text"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={errors.password ? 'error' : ''}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
}
Real-time Validation
function RealTimeValidationForm() {
const [username, setUsername] = useState('');
const [usernameError, setUsernameError] = useState('');
const [isChecking, setIsChecking] = useState(false);
// Debounced username check
useEffect(() => {
if (!username) {
setUsernameError('');
return;
}
const timeoutId = setTimeout(async () => {
setIsChecking(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500));
if (username === 'admin' || username === 'user') {
setUsernameError('Username is already taken');
} else {
setUsernameError('');
}
} finally {
setIsChecking(false);
}
}, 500);
return () => clearTimeout(timeoutId);
}, [username]);
return (
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
{isChecking && <span>Checking...</span>}
{usernameError && <span className="error">{usernameError}</span>}
{!isChecking && !usernameError && username &&
<span className="success">Username available!</span>
}
</div>
);
}
Uncontrolled Components
function UncontrolledForm() {
const formRef = useRef();
const inputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
// Access form data
const formData = new FormData(formRef.current);
const data = Object.fromEntries(formData.entries());
console.log('Form data:', data);
// Access specific input
console.log('Input value:', inputRef.current.value);
};
const focusInput = () => {
inputRef.current.focus();
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
ref={inputRef}
defaultValue="John"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
defaultValue="john@example.com"
/>
</div>
<button type="submit">Submit</button>
<button type="button" onClick={focusInput}>
Focus Name Input
</button>
</form>
);
}
Custom Form Hook
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setValues(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({
...prev,
[name]: true
}));
// Validate on blur
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
}
};
const handleSubmit = (onSubmit) => async (e) => {
e.preventDefault();
setIsSubmitting(true);
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
await onSubmit(values);
}
} else {
await onSubmit(values);
}
setIsSubmitting(false);
};
const resetForm = () => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
};
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
resetForm
};
}
// Usage of custom hook
function LoginForm() {
const validate = (values) => {
const errors = {};
if (!values.email) {
errors.email = 'Email is required';
}
if (!values.password) {
errors.password = 'Password is required';
}
return errors;
};
const {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit
} = useForm({ email: '', password: '' }, validate);
const onSubmit = async (values) => {
console.log('Logging in with:', values);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Password"
/>
{touched.password && errors.password && (
<span className="error">{errors.password}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
Form Libraries
Popular Form Libraries
Advanced Form Patterns
Dynamic Form Fields
function DynamicForm() {
const [fields, setFields] = useState([{ id: 1, value: '' }]);
const addField = () => {
setFields([...fields, { id: Date.now(), value: '' }]);
};
const removeField = (id) => {
setFields(fields.filter(field => field.id !== id));
};
const updateField = (id, value) => {
setFields(fields.map(field =>
field.id === id ? { ...field, value } : field
));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form values:', fields.map(f => f.value));
};
return (
<form onSubmit={handleSubmit}>
{fields.map((field, index) => (
<div key={field.id}>
<input
type="text"
value={field.value}
onChange={(e) => updateField(field.id, e.target.value)}
placeholder={`Field ${index + 1}`}
/>
{fields.length > 1 && (
<button
type="button"
onClick={() => removeField(field.id)}
>
Remove
</button>
)}
</div>
))}
<button type="button" onClick={addField}>
Add Field
</button>
<button type="submit">Submit</button>
</form>
);
}
Multi-Step Form
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
// Step 1
firstName: '',
lastName: '',
// Step 2
email: '',
phone: '',
// Step 3
address: '',
city: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const nextStep = () => setStep(prev => prev + 1);
const prevStep = () => setStep(prev => prev - 1);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Final form data:', formData);
};
const renderStep = () => {
switch(step) {
case 1:
return (
<div>
<h3>Step 1: Personal Info</h3>
<input
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="First Name"
/>
<input
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Last Name"
/>
</div>
);
case 2:
return (
<div>
<h3>Step 2: Contact Info</h3>
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="Phone"
/>
</div>
);
case 3:
return (
<div>
<h3>Step 3: Address</h3>
<input
name="address"
value={formData.address}
onChange={handleChange}
placeholder="Address"
/>
<input
name="city"
value={formData.city}
onChange={handleChange}
placeholder="City"
/>
</div>
);
default:
return null;
}
};
return (
<form onSubmit={handleSubmit}>
{renderStep()}
<div>
{step > 1 && (
<button type="button" onClick={prevStep}>
Previous
</button>
)}
{step < 3 ? (
<button type="button" onClick={nextStep} >
Next
</button>
) : (
<button type="submit">Submit</button>
)}
</div>
</form>
);
}