Working with Forms
Learn how to build interactive forms with validation, state management, and submission handling in Next.js.
What You'll Learn
- Form basics in React
- Managing form state with hooks
- Form validation
- Handling submissions
- Displaying errors and success messages
Form Basics
Simple Form Component
"use client";
export default function ContactForm() {
return (
<form className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
Name
</label>
<input
type="text"
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg"
>
Submit
</button>
</form>
);
}
Managing Form State
Using useState
"use client";
import { useState } from 'react';
export default function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
return (
<form className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Message
</label>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={4}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Send Message
</button>
</form>
);
}
Form Submission
Handle Submit Event
"use client";
import { useState } from 'react';
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); // Prevent page reload
console.log('Form submitted:', formData);
// Send to API
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
alert('Message sent!');
// Reset form
setFormData({ name: '', email: '', message: '' });
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Your name"
className="w-full px-4 py-2 border rounded-lg"
/>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Your email"
className="w-full px-4 py-2 border rounded-lg"
/>
<textarea
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
placeholder="Your message"
rows={4}
className="w-full px-4 py-2 border rounded-lg"
/>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Send Message
</button>
</form>
);
}
Form Validation
Client-Side Validation
"use client";
import { useState } from 'react';
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState({
name: '',
email: '',
message: '',
});
const validate = () => {
const newErrors = { name: '', email: '', message: '' };
let isValid = true;
// Name validation
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
isValid = false;
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
isValid = false;
} else if (!emailRegex.test(formData.email)) {
newErrors.email = 'Invalid email address';
isValid = false;
}
// Message validation
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
isValid = false;
} else if (formData.message.length < 10) {
newErrors.message = 'Message must be at least 10 characters';
isValid = false;
}
setErrors(newErrors);
return isValid;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) {
return; // Don't submit if validation fails
}
// Submit form...
console.log('Form is valid, submitting...');
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
Name *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className={`w-full px-4 py-2 border rounded-lg ${
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-2">
Email *
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className={`w-full px-4 py-2 border rounded-lg ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-2">
Message *
</label>
<textarea
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
rows={4}
className={`w-full px-4 py-2 border rounded-lg ${
errors.message ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.message && (
<p className="text-red-500 text-sm mt-1">{errors.message}</p>
)}
</div>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Send Message
</button>
</form>
);
}
Loading States
Show Loading While Submitting
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
setIsSubmitting(true);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
alert('Message sent successfully!');
setFormData({ name: '', email: '', message: '' });
} else {
alert('Failed to send message');
}
} catch (error) {
alert('An error occurred');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* ... form fields ... */}
<button
type="submit"
disabled={isSubmitting}
className={`px-6 py-3 rounded-lg ${
isSubmitting
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
} text-white`}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
Success/Error Messages
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData),
});
if (response.ok) {
setSubmitStatus('success');
setFormData({ name: '', email: '', message: '' });
} else {
setSubmitStatus('error');
}
} catch (error) {
setSubmitStatus('error');
}
};
return (
<div>
{submitStatus === 'success' && (
<div className="mb-4 p-4 bg-green-100 text-green-900 rounded-lg">
Message sent successfully!
</div>
)}
{submitStatus === 'error' && (
<div className="mb-4 p-4 bg-red-100 text-red-900 rounded-lg">
Failed to send message. Please try again.
</div>
)}
<form onSubmit={handleSubmit}>
{/* ... form fields ... */}
</form>
</div>
);
Form Best Practices
1. Always Validate
// ✅ Client-side validation for UX
// ✅ Server-side validation for security
2. Provide Clear Feedback
// ✅ Error messages
// ✅ Loading states
// ✅ Success confirmation
3. Accessible Forms
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
/>
4. Disable Submit While Loading
<button type="submit" disabled={isSubmitting}>
Quick Reference
Form with state:
const [value, setValue] = useState('');
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
Handle submit:
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Process form
};
<form onSubmit={handleSubmit}>
Show errors:
{error && <p className="text-red-500">{error}</p>}
What's Next
Understanding TypeScript Types →