Forms in React
Forms are essential for user input. React provides controlled components for form handling, giving you full control over form state and validation.
🎯 Controlled vs Uncontrolled
Controlled Component (Recommended):
// React controls the input value
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
Uncontrolled Component:
// DOM controls the input value
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => {
console.log(inputRef.current.value);
};
return <input ref={inputRef} />;
}
📋 Basic Form
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitted:', formData);
// Submit to API
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Send</button>
</form>
);
}
✅ Validation Patterns
Real-time Validation:
function SignupForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// Validation functions
const validateUsername = (value) => {
if (!value) return 'Username is required';
if (value.length < 3) return 'Username must be at least 3 characters';
if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Username can only contain letters, numbers, and underscores';
return '';
};
const validateEmail = (value) => {
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Email is invalid';
return '';
};
const validatePassword = (value) => {
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 (!/[a-z]/.test(value)) return 'Password must contain lowercase letter';
if (!/[0-9]/.test(value)) return 'Password must contain number';
return '';
};
const validateConfirmPassword = (value) => {
if (value !== formData.password) return 'Passwords do not match';
return '';
};
// Validate field
const validateField = (name, value) => {
let error = '';
switch (name) {
case 'username':
error = validateUsername(value);
break;
case 'email':
error = validateEmail(value);
break;
case 'password':
error = validatePassword(value);
break;
case 'confirmPassword':
error = validateConfirmPassword(value);
break;
}
setErrors(prev => ({ ...prev, [name]: error }));
return error;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Validate if field has been touched
if (touched[name]) {
validateField(name, value);
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
validateField(name, value);
};
const handleSubmit = (e) => {
e.preventDefault();
// Mark all as touched
const allTouched = Object.keys(formData).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {});
setTouched(allTouched);
// Validate all fields
const newErrors = {};
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key]);
if (error) newErrors[key] = error;
});
// Submit if no errors
if (Object.keys(newErrors).length === 0) {
console.log('Form is valid:', formData);
// API call here
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="username"
value={formData.username}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Username"
/>
{touched.username && errors.username && (
<span className="error">{errors.username}</span>
)}
</div>
<div>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Password"
/>
{touched.password && errors.password && (
<span className="error">{errors.password}</span>
)}
</div>
<div>
<input
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Confirm Password"
/>
{touched.confirmPassword && errors.confirmPassword && (
<span className="error">{errors.confirmPassword}</span>
)}
</div>
<button type="submit">Sign Up</button>
</form>
);
}
📚 Using Formik Library
// npm install formik yup
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const validationSchema = Yup.object({
name: Yup.string()
.min(2, 'Too short')
.max(50, 'Too long')
.required('Required'),
email: Yup.string()
.email('Invalid email')
.required('Required'),
age: Yup.number()
.min(18, 'Must be 18+')
.required('Required')
});
function FormikExample() {
return (
<Formik
initialValues={{
name: '',
email: '',
age: ''
}}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
console.log(values);
setSubmitting(false);
}, 1000);
}}
>
{({ isSubmitting }) => (
<Form>
<div>
<Field name="name" placeholder="Name" />
<ErrorMessage name="name" component="span" className="error" />
</div>
<div>
<Field name="email" type="email" placeholder="Email" />
<ErrorMessage name="email" component="span" className="error" />
</div>
<div>
<Field name="age" type="number" placeholder="Age" />
<ErrorMessage name="age" component="span" className="error" />
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</Form>
)}
</Formik>
);
}
🎨 React Hook Form
// npm install react-hook-form
import { useForm } from 'react-hook-form';
function RHFExample() {
const {
register,
handleSubmit,
formState: { errors },
watch
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
// Watch field value
const password = watch('password');
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Invalid email'
}
})}
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 8+ characters'
}
})}
placeholder="Password"
/>
{errors.password && <span>{errors.password.message}</span>}
</div>
<div>
<input
type="password"
{...register('confirmPassword', {
validate: value => value === password || 'Passwords do not match'
})}
placeholder="Confirm Password"
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
}
📱 File Upload
function FileUpload() {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState('');
const [uploading, setUploading] = useState(false);
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (!selectedFile) return;
// Validate file type
if (!selectedFile.type.startsWith('image/')) {
alert('Please select an image file');
return;
}
// Validate file size (5MB max)
if (selectedFile.size > 5 * 1024 * 1024) {
alert('File must be less than 5MB');
return;
}
setFile(selectedFile);
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(selectedFile);
};
const handleUpload = async () => {
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
console.log('Uploaded:', data.url);
alert('Upload successful!');
} catch (error) {
alert('Upload failed');
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
/>
{preview && (
<div>
<h3>Preview:</h3>
<img src={preview} alt="Preview" style={{ maxWidth: '300px' }} />
</div>
)}
{file && (
<div>
<p>{file.name} ({(file.size / 1024).toFixed(2)} KB)</p>
<button onClick={handleUpload} disabled={uploading}>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
)}
</div>
);
}
🔍 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('Values:', fields.map(f => f.value));
};
return (
<form onSubmit={handleSubmit}>
{fields.map((field, index) => (
<div key={field.id}>
<input
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>
);
}
🎯 Key Takeaways
- Controlled components: React manages form state
- e.preventDefault(): Stop form submission reload
- Validation: Real-time or on submit
- Error messages: Show after field touched
- Formik/RHF: Libraries simplify complex forms
- File uploads: Use FormData API
- Dynamic fields: Array state for add/remove
- Accessibility: Labels, aria attributes