Forms and User Input Handling
1. Controlled Components and Input State Management
| Input Type | Pattern | State Binding | Event Handler |
|---|---|---|---|
| Text Input | <input value={state} onChange={handler} /> |
State holds current value | e.target.value |
| Textarea | <textarea value={state} onChange={handler} /> |
Same as text input | e.target.value |
| Checkbox | <input type="checkbox" checked={state} /> |
Boolean state | e.target.checked |
| Radio | <input type="radio" checked={state === value} /> |
Compare state to value | e.target.value |
| Select | <select value={state} onChange={handler} /> |
Selected option value | e.target.value |
| Multiple Select | <select multiple value={array} /> |
Array of values | Array.from(e.target.selectedOptions) |
Example: Controlled component patterns
// Basic text input
const TextInput = () => {
const [value, setValue] = useState('');
return (
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter text"
/>
);
};
// Multiple inputs with single state object
const Form = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
age: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="age"
type="number"
value={formData.age}
onChange={handleChange}
placeholder="Age"
/>
</form>
);
};
// Checkbox example
const CheckboxInput = () => {
const [accepted, setAccepted] = useState(false);
return (
<label>
<input
type="checkbox"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
/>
I accept the terms
</label>
);
};
// Multiple checkboxes (array)
const MultiCheckbox = () => {
const [interests, setInterests] = useState([]);
const handleToggle = (interest) => {
setInterests(prev =>
prev.includes(interest)
? prev.filter(i => i !== interest)
: [...prev, interest]
);
};
return (
<div>
{['React', 'Vue', 'Angular'].map(tech => (
<label key={tech}>
<input
type="checkbox"
checked={interests.includes(tech)}
onChange={() => handleToggle(tech)}
/>
{tech}
</label>
))}
</div>
);
};
// Radio buttons
const RadioInput = () => {
const [plan, setPlan] = useState('basic');
return (
<div>
{['basic', 'pro', 'enterprise'].map(value => (
<label key={value}>
<input
type="radio"
name="plan"
value={value}
checked={plan === value}
onChange={(e) => setPlan(e.target.value)}
/>
{value}
</label>
))}
</div>
);
};
// Select dropdown
const SelectInput = () => {
const [country, setCountry] = useState('');
return (
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="">Select country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
);
};
Note: Controlled components have React state as the "single source of truth". Every state
mutation
happens through setState, providing full control and predictability.
2. Uncontrolled Components and useRef
| Approach | Implementation | When to Use | Access Pattern |
|---|---|---|---|
| useRef | const ref = useRef(null) |
Direct DOM access needed | ref.current.value |
| defaultValue | <input defaultValue="text" /> |
Initial value only | No state synchronization |
| File Input | Always uncontrolled | Security reasons | ref.current.files |
| Form Reset | formRef.current.reset() |
Native form methods | Reset all fields at once |
| Integration | Third-party libraries | Non-React code | Direct DOM manipulation |
Example: Uncontrolled components with useRef
import { useRef } from 'react';
// Basic uncontrolled input
const UncontrolledInput = () => {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Value:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="Initial value" />
<button type="submit">Submit</button>
</form>
);
};
// File input (always uncontrolled)
const FileInput = () => {
const fileRef = useRef(null);
const handleUpload = () => {
const files = fileRef.current.files;
if (files.length > 0) {
console.log('Selected file:', files[0].name);
}
};
return (
<div>
<input type="file" ref={fileRef} />
<button onClick={handleUpload}>Upload</button>
</div>
);
};
// Form with multiple refs
const UncontrolledForm = () => {
const nameRef = useRef(null);
const emailRef = useRef(null);
const formRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
name: nameRef.current.value,
email: emailRef.current.value
};
console.log('Form data:', formData);
// Reset form using native method
formRef.current.reset();
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<input ref={nameRef} name="name" defaultValue="" />
<input ref={emailRef} name="email" type="email" defaultValue="" />
<button type="submit">Submit</button>
<button type="button" onClick={() => formRef.current.reset()}>
Clear
</button>
</form>
);
};
// Hybrid approach (controlled + ref for advanced features)
const HybridInput = () => {
const [value, setValue] = useState('');
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus();
};
const handleSelectAll = () => {
inputRef.current.select();
};
return (
<div>
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button onClick={handleFocus}>Focus</button>
<button onClick={handleSelectAll}>Select All</button>
</div>
);
};
// Integration with third-party library
const ThirdPartyInput = () => {
const containerRef = useRef(null);
useEffect(() => {
// Initialize third-party library
const instance = ExternalLibrary.init(containerRef.current, {
onChange: (value) => {
console.log('Value changed:', value);
}
});
return () => {
instance.destroy();
};
}, []);
return <div ref={containerRef} />;
};
Warning: Prefer controlled components for most use cases. Use uncontrolled only for file
inputs,
integration with non-React code, or when you need direct DOM manipulation.
3. Form Validation Patterns and Error Handling
| Strategy | When to Validate | Implementation | User Experience |
|---|---|---|---|
| On Submit | Form submission | Validate all fields in submit handler | Less intrusive, batch errors |
| On Blur | Field loses focus | Validate in onBlur handler | Immediate feedback after editing |
| On Change | Every keystroke | Validate in onChange handler | Real-time feedback (can be annoying) |
| Debounced | After pause in typing | useDebounce + validation | Balance between immediate and batch |
| Mixed | Different rules per field | Combine strategies | Optimal UX per field type |
Example: Form validation patterns
// Validation on submit
const SubmitValidation = () => {
const [formData, setFormData] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
// Submit form
console.log('Valid form data:', formData);
setErrors({});
};
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>
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
};
// Validation on blur (after editing)
const BlurValidation = () => {
const [email, setEmail] = useState('');
const [touched, setTouched] = useState(false);
const [error, setError] = useState('');
const validateEmail = (value) => {
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
return '';
};
const handleBlur = () => {
setTouched(true);
setError(validateEmail(email));
};
return (
<div>
<input
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (touched) setError(validateEmail(e.target.value));
}}
onBlur={handleBlur}
placeholder="Email"
/>
{touched && error && <span className="error">{error}</span>}
</div>
);
};
// Custom validation hook
const useFormValidation = (initialState, validationRules) => {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (fieldName, value) => {
const rule = validationRules[fieldName];
if (!rule) return '';
for (const validator of rule) {
const error = validator(value, values);
if (error) return error;
}
return '';
};
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
if (touched[name]) {
const error = validate(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const error = validate(name, values[name]);
setErrors(prev => ({ ...prev, [name]: error }));
};
const validateAll = () => {
const newErrors = {};
Object.keys(validationRules).forEach(field => {
const error = validate(field, values[field]);
if (error) newErrors[field] = error;
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateAll
};
};
// Usage
const ValidationForm = () => {
const { values, errors, touched, handleChange, handleBlur, validateAll } =
useFormValidation(
{ email: '', password: '' },
{
email: [
(v) => !v ? 'Required' : '',
(v) => !/\S+@\S+\.\S+/.test(v) ? 'Invalid email' : ''
],
password: [
(v) => !v ? 'Required' : '',
(v) => v.length < 8 ? 'Min 8 characters' : ''
]
}
);
const handleSubmit = (e) => {
e.preventDefault();
if (validateAll()) {
console.log('Valid:', values);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
<button type="submit">Submit</button>
</form>
);
};
4. Custom Input Components and Hooks
| Pattern | Purpose | Benefits | Example |
|---|---|---|---|
| Wrapper Components | Consistent styling and behavior | Reusability, DRY, branding | Custom TextInput with label/error |
| useInput Hook | Extract input state logic | Cleaner components, reusable logic | Hook returning value, onChange, reset |
| useForm Hook | Manage entire form state | Centralized form logic | Hook with values, errors, handlers |
| Compound Components | Flexible input structure | Composable API, flexibility | Input.Label, Input.Field, Input.Error |
| Controlled Wrappers | Abstract third-party inputs | Consistent API, testability | Wrap date picker with standard props |
Example: Custom input components and hooks
// Custom input hook
const useInput = (initialValue = '', validation) => {
const [value, setValue] = useState(initialValue);
const [error, setError] = useState('');
const [touched, setTouched] = useState(false);
const onChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
if (touched && validation) {
setError(validation(newValue));
}
};
const onBlur = () => {
setTouched(true);
if (validation) {
setError(validation(value));
}
};
const reset = () => {
setValue(initialValue);
setError('');
setTouched(false);
};
return {
value,
error,
touched,
onChange,
onBlur,
reset,
bind: { value, onChange, onBlur }
};
};
// Usage
const LoginForm = () => {
const email = useInput('', (v) =>
!v ? 'Required' : !/\S+@\S+/.test(v) ? 'Invalid' : ''
);
const password = useInput('', (v) =>
v.length < 8 ? 'Min 8 chars' : ''
);
const handleSubmit = (e) => {
e.preventDefault();
if (!email.error && !password.error) {
console.log({ email: email.value, password: password.value });
}
};
return (
<form onSubmit={handleSubmit}>
<input {...email.bind} placeholder="Email" />
{email.touched && email.error && <span>{email.error}</span>}
<input {...password.bind} type="password" placeholder="Password" />
{password.touched && password.error && <span>{password.error}</span>}
<button type="submit">Login</button>
</form>
);
};
// Custom input wrapper component
const TextField = ({ label, error, touched, ...props }) => {
return (
<div className="field">
{label && <label>{label}</label>}
<input {...props} className={error && touched ? 'error' : ''} />
{touched && error && (
<span className="error-message">{error}</span>
)}
</div>
);
};
// Usage
const FormWithTextField = () => {
const email = useInput('');
return (
<TextField
label="Email"
type="email"
error={email.error}
touched={email.touched}
{...email.bind}
/>
);
};
// Compound component pattern
const Input = ({ children, error, touched }) => {
return (
<div className="input-group">
{children}
{touched && error && <Input.Error>{error}</Input.Error>}
</div>
);
};
Input.Label = ({ children, htmlFor }) => (
<label htmlFor={htmlFor} className="input-label">{children}</label>
);
Input.Field = ({ id, ...props }) => (
<input id={id} className="input-field" {...props} />
);
Input.Error = ({ children }) => (
<span className="input-error">{children}</span>
);
// Usage
const CompoundForm = () => {
const email = useInput('');
return (
<Input error={email.error} touched={email.touched}>
<Input.Label htmlFor="email">Email</Input.Label>
<Input.Field id="email" type="email" {...email.bind} />
</Input>
);
};
// Custom form hook
const useForm = (initialValues, validationSchema) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = (name, value) => {
if (validationSchema[name]) {
return validationSchema[name](value, values);
}
return '';
};
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
if (touched[name]) {
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const error = validateField(name, values[name]);
setErrors(prev => ({ ...prev, [name]: error }));
};
const handleSubmit = async (onSubmit) => {
return async (e) => {
e.preventDefault();
// Validate all fields
const newErrors = {};
Object.keys(validationSchema).forEach(field => {
const error = validateField(field, values[field]);
if (error) newErrors[field] = error;
});
setErrors(newErrors);
setTouched(Object.keys(validationSchema).reduce(
(acc, key) => ({ ...acc, [key]: true }), {}
));
if (Object.keys(newErrors).length === 0) {
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
}
};
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
};
};
5. Form Libraries Integration (Formik, React Hook Form)
| Library | Philosophy | Pros | Cons |
|---|---|---|---|
| Formik | Component-based, render props | Easy to learn, good DX, field-level validation | More re-renders, larger bundle |
| React Hook Form | Hook-based, uncontrolled inputs | High performance, small bundle, less re-renders | Steeper learning curve, ref-based |
| Final Form | Framework-agnostic core | Flexible, powerful, subscription-based | More complex API |
| Yup / Zod | Schema validation libraries | Type-safe, reusable schemas, great DX | Additional dependency |
Example: Formik and React Hook Form
// FORMIK Example
import { useFormik } from 'formik';
import * as Yup from 'yup';
const FormikForm = () => {
const formik = useFormik({
initialValues: {
email: '',
password: ''
},
validationSchema: Yup.object({
email: Yup.string()
.email('Invalid email')
.required('Required'),
password: Yup.string()
.min(8, 'Must be 8 characters or more')
.required('Required')
}),
onSubmit: async (values) => {
await api.login(values);
}
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
type="email"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
/>
{formik.touched.email && formik.errors.email && (
<div>{formik.errors.email}</div>
)}
<input
name="password"
type="password"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.password}
/>
{formik.touched.password && formik.errors.password && (
<div>{formik.errors.password}</div>
)}
<button type="submit" disabled={formik.isSubmitting}>
Submit
</button>
</form>
);
};
// Formik with Formik components
import { Formik, Form, Field, ErrorMessage } from 'formik';
const FormikComponents = () => (
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={validationSchema}
onSubmit={async (values) => {
await api.login(values);
}}
>
{({ isSubmitting }) => (
<Form>
<Field name="email" type="email" />
<ErrorMessage name="email" component="div" />
<Field name="password" type="password" />
<ErrorMessage name="password" component="div" />
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</Form>
)}
</Formik>
);
// REACT HOOK FORM Example
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email').min(1, 'Required'),
password: z.string().min(8, 'Must be at least 8 characters')
});
const ReactHookForm = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm({
resolver: zodResolver(schema)
});
const onSubmit = async (data) => {
await api.login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email')}
type="email"
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
<input
{...register('password')}
type="password"
placeholder="Password"
/>
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
);
};
// React Hook Form with Controller (for custom components)
import { Controller } from 'react-hook-form';
const RHFWithController = () => {
const { control, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="customInput"
control={control}
rules={{ required: 'This field is required' }}
render={({ field, fieldState }) => (
<div>
<CustomInput {...field} />
{fieldState.error && <span>{fieldState.error.message}</span>}
</div>
)}
/>
<button type="submit">Submit</button>
</form>
);
};
Note: React Hook Form is generally recommended for performance-critical apps due to fewer
re-renders.
Formik offers easier learning curve and better integration with controlled components.
6. File Upload and FormData Handling
| Feature | Implementation | Key Points | Browser API |
|---|---|---|---|
| File Input | <input type="file" ref={fileRef} /> |
Always uncontrolled | Access via ref.current.files |
| Multiple Files | <input type="file" multiple /> |
files is FileList (array-like) | Array.from(files) |
| File Preview | URL.createObjectURL(file) |
Create temporary URL | Revoke with URL.revokeObjectURL |
| FormData | new FormData() |
Build multipart/form-data | Append files and fields |
| Drag and Drop | onDrop, onDragOver handlers | e.dataTransfer.files | preventDefault() required |
| Progress | XMLHttpRequest or axios | onUploadProgress callback | Track upload percentage |
Example: File upload and FormData
// Basic file upload
const FileUpload = () => {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
setFile(selectedFile);
// Create preview URL for images
if (selectedFile.type.startsWith('image/')) {
const url = URL.createObjectURL(selectedFile);
setPreview(url);
}
}
};
const handleUpload = async () => {
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My file');
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
console.log('Upload success:', data);
} catch (error) {
console.error('Upload failed:', error);
}
};
// Cleanup preview URL
useEffect(() => {
return () => {
if (preview) URL.revokeObjectURL(preview);
};
}, [preview]);
return (
<div>
<input type="file" onChange={handleFileChange} accept="image/*" />
{preview && <img src={preview} alt="Preview" style={{ width: 200 }} />}
{file && (
<div>
<p>File: {file.name}</p>
<p>Size: {(file.size / 1024).toFixed(2)} KB</p>
<button onClick={handleUpload}>Upload</button>
</div>
)}
</div>
);
};
// Multiple file upload
const MultiFileUpload = () => {
const [files, setFiles] = useState([]);
const handleFileChange = (e) => {
const fileList = Array.from(e.target.files);
setFiles(fileList);
};
const handleUpload = async () => {
const formData = new FormData();
files.forEach((file, index) => {
formData.append(\`files[\${index}]\`, file);
});
await fetch('/api/upload-multiple', {
method: 'POST',
body: formData
});
};
return (
<div>
<input type="file" multiple onChange={handleFileChange} />
<ul>
{files.map((file, index) => (
<li key={index}>{file.name} - {file.size} bytes</li>
))}
</ul>
{files.length > 0 && (
<button onClick={handleUpload}>Upload All</button>
)}
</div>
);
};
// Drag and drop file upload
const DragDropUpload = () => {
const [files, setFiles] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const handleDragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
setFiles(prev => [...prev, ...droppedFiles]);
};
return (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
border: isDragging ? '2px solid blue' : '2px dashed gray',
padding: '2rem',
textAlign: 'center'
}}
>
{files.length === 0 ? (
<p>Drag and drop files here</p>
) : (
<ul>
{files.map((file, i) => (
<li key={i}>{file.name}</li>
))}
</ul>
)}
</div>
);
};
// Upload with progress
const UploadWithProgress = () => {
const [file, setFile] = useState(null);
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
try {
// Using axios for upload progress
const response = await axios.post('/api/upload', formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setProgress(percentCompleted);
}
});
console.log('Upload complete:', response.data);
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
setProgress(0);
}
};
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files[0])}
disabled={uploading}
/>
{uploading && (
<div>
<progress value={progress} max="100" />
<span>{progress}%</span>
</div>
)}
{file && !uploading && (
<button onClick={handleUpload}>Upload</button>
)}
</div>
);
};
// Complete form with file and other fields
const CompleteForm = () => {
const [formData, setFormData] = useState({
title: '',
description: '',
file: null
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleFileChange = (e) => {
setFormData(prev => ({ ...prev, file: e.target.files[0] }));
};
const handleSubmit = async (e) => {
e.preventDefault();
const data = new FormData();
data.append('title', formData.title);
data.append('description', formData.description);
if (formData.file) {
data.append('file', formData.file);
}
await fetch('/api/submit', {
method: 'POST',
body: data
});
};
return (
<form onSubmit={handleSubmit}>
<input
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Title"
/>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Description"
/>
<input type="file" onChange={handleFileChange} />
<button type="submit">Submit</button>
</form>
);
};
Warning: Always validate file types and sizes on the server. Client-side validation (accept
attribute)
is only for UX. Remember to revoke object URLs with
URL.revokeObjectURL() to prevent memory leaks.
Forms Best Practices
- Controlled components: Prefer for most inputs - React state as single source of truth
- Validation timing: Validate on blur for better UX, on submit as final check
- Form libraries: Use React Hook Form for performance, Formik for easier DX
- Custom hooks: Extract form logic for reusability and cleaner components
- File uploads: Always uncontrolled, use FormData, validate server-side
- Error messages: Show after touched, clear on change, batch on submit