Next.js Form Basics
Forms are a fundamental part of web applications, allowing users to input data and interact with your application. In this guide, we'll explore how to implement and work with forms in Next.js applications, from basic setup to handling form submissions.
Introduction to Forms in Next.js
Next.js, being built on top of React, handles forms similarly to React but with some additional features that make data submission and validation more efficient. Understanding form management in Next.js is essential for building interactive web applications.
In this tutorial, you'll learn:
- How to create basic forms in Next.js
- Different ways to handle form submission
- Managing form state
- Basic form validation techniques
- Best practices for form implementation
Creating a Basic Form
Let's start with creating a simple form in Next.js:
// app/components/SimpleForm.jsx
"use client";
import { useState } from 'react';
export default function SimpleForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
// Here you would typically send the data to an API
};
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-4 bg-white rounded shadow">
<h2 className="text-xl mb-4">Contact Form</h2>
<div className="mb-4">
<label htmlFor="name" className="block mb-1">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full p-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label htmlFor="email" className="block mb-1">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className="w-full p-2 border rounded"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Submit
</button>
</form>
);
}
In this example:
- We create a form with two fields: name and email
- We use React's
useState
to manage form data - The
handleChange
function updates state as users type - The
handleSubmit
function processes the form data when submitted - We prevent the default form submission behavior with
e.preventDefault()
Notice the "use client"
directive at the top of the file. This is necessary because forms typically involve client-side interactivity like state management.
Form Submission Methods in Next.js
Next.js provides multiple ways to handle form submissions. Let's explore the most common approaches:
1. Client-Side Form Handling
This is what we saw in our first example, where the form data is handled entirely on the client side using React state.
2. Server Actions (Next.js 13.4+)
Next.js introduced Server Actions, which allow you to define functions that run on the server directly from your components:
// app/components/ServerActionForm.jsx
"use client";
import { useState } from 'react';
// This would be imported from a separate file in a real application
async function submitToServer(formData) {
'use server';
// Server-side validation and processing
const name = formData.get('name');
const email = formData.get('email');
// Do something with the data (e.g., save to database)
console.log('Server received:', { name, email });
// Return a result
return { success: true };
}
export default function ServerActionForm() {
const [message, setMessage] = useState('');
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
try {
// Call the server action
const result = await submitToServer(formData);
setMessage(result.success ? 'Form submitted successfully!' : 'Submission failed');
} catch (error) {
setMessage('An error occurred');
console.error(error);
}
}
return (
<div className="max-w-md mx-auto p-4 bg-white rounded shadow">
{message && (
<div className="mb-4 p-2 bg-green-100 border border-green-400 text-green-700 rounded">
{message}
</div>
)}
<form onSubmit={handleSubmit}>
<h2 className="text-xl mb-4">Contact Us</h2>
<div className="mb-4">
<label htmlFor="name" className="block mb-1">Name:</label>
<input
type="text"
id="name"
name="name"
className="w-full p-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label htmlFor="email" className="block mb-1">Email:</label>
<input
type="email"
id="email"
name="email"
className="w-full p-2 border rounded"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Submit
</button>
</form>
</div>
);
}
Server Actions are a newer feature in Next.js and require version 13.4 or later. The 'use server'
directive marks functions that execute on the server.
3. API Routes
Another common approach is to submit forms to your own API routes:
// app/components/ApiRouteForm.jsx
"use client";
import { useState } from 'react';
export default function ApiRouteForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [status, setStatus] = useState('');
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setStatus('submitting');
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Something went wrong');
}
setStatus('success');
setFormData({ name: '', email: '', message: '' });
} catch (error) {
console.error('Submission error:', error);
setStatus('error');
}
};
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-4 bg-white rounded shadow">
<h2 className="text-xl mb-4">Contact Us</h2>
{status === 'success' && (
<div className="mb-4 p-2 bg-green-100 text-green-700 rounded">
Thank you! Your message has been sent.
</div>
)}
{status === 'error' && (
<div className="mb-4 p-2 bg-red-100 text-red-700 rounded">
Failed to send message. Please try again.
</div>
)}
<div className="mb-4">
<label htmlFor="name" className="block mb-1">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full p-2 border rounded"
required
disabled={status === 'submitting'}
/>
</div>
<div className="mb-4">
<label htmlFor="email" className="block mb-1">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className="w-full p-2 border rounded"
required
disabled={status === 'submitting'}
/>
</div>
<div className="mb-4">
<label htmlFor="message" className="block mb-1">Message:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
className="w-full p-2 border rounded"
rows="4"
required
disabled={status === 'submitting'}
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600 disabled:bg-blue-300"
disabled={status === 'submitting'}
>
{status === 'submitting' ? 'Sending...' : 'Send Message'}
</button>
</form>
);
}
Then, create an API route to handle the form submission:
// app/api/contact/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
try {
// Parse the request body
const body = await request.json();
// Validate form data
const { name, email, message } = body;
if (!name || !email || !message) {
return NextResponse.json(
{ message: 'Missing required fields' },
{ status: 400 }
);
}
// Process the data (e.g., send an email, store in database, etc.)
console.log('Received form submission:', body);
// In a real application, you might:
// - Send an email using a service like SendGrid or Nodemailer
// - Store the contact in a database
// - Forward to a CRM system
return NextResponse.json({
message: 'Form submitted successfully'
});
} catch (error) {
console.error('Contact form error:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}
Basic Form Validation
Form validation is crucial for ensuring users provide correct data. Here's how to implement basic validation:
Client-side Validation
// app/components/ValidatedForm.jsx
"use client";
import { useState } from 'react';
export default function ValidatedForm() {
const [formData, setFormData] = useState({
username: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState({});
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]: ''
}));
}
};
const validateForm = () => {
const newErrors = {};
// Username validation
if (!formData.username) {
newErrors.username = 'Username is required';
} else if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}
// 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';
}
// Confirm password validation
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form is valid, submitting:', formData);
// Submit the form data
}
};
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-4 bg-white rounded shadow">
<h2 className="text-xl mb-4">Create Account</h2>
<div className="mb-4">
<label htmlFor="username" className="block mb-1">Username:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={`w-full p-2 border rounded ${errors.username ? 'border-red-500' : ''}`}
/>
{errors.username && (
<p className="text-red-500 text-sm mt-1">{errors.username}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="password" className="block mb-1">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`w-full p-2 border rounded ${errors.password ? 'border-red-500' : ''}`}
/>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="confirmPassword" className="block mb-1">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
className={`w-full p-2 border rounded ${errors.confirmPassword ? 'border-red-500' : ''}`}
/>
{errors.confirmPassword && (
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
)}
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Register
</button>
</form>
);
}
Real-World Example: Newsletter Subscription Form
Let's create a practical example of a newsletter subscription form with feedback:
// app/components/NewsletterForm.jsx
"use client";
import { useState } from 'react';
export default function NewsletterForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState('idle'); // idle, loading, success, error
const [message, setMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setStatus('loading');
try {
// Validate email format
if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw new Error('Please enter a valid email address');
}
// In a real application, this would be an API call
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
// Success case
setStatus('success');
setMessage('Thank you for subscribing to our newsletter!');
setEmail('');
} catch (error) {
setStatus('error');
setMessage(error.message || 'Failed to subscribe. Please try again.');
}
};
return (
<div className="max-w-md mx-auto p-6 bg-gray-50 rounded-lg shadow">
<h2 className="text-2xl font-bold mb-4">Subscribe to Our Newsletter</h2>
<p className="text-gray-600 mb-6">
Get the latest updates, tips, and exclusive content delivered straight to your inbox.
</p>
{status === 'success' ? (
<div className="bg-green-100 p-4 rounded mb-4">
<p className="text-green-700">{message}</p>
<button
className="mt-3 text-sm text-green-700 underline"
onClick={() => setStatus('idle')}
>
Subscribe another email
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block mb-1 text-gray-700">
Email Address:
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
className="w-full p-3 border rounded focus:ring focus:ring-blue-200 focus:border-blue-500"
required
disabled={status === 'loading'}
/>
</div>
{status === 'error' && (
<div className="bg-red-100 p-3 rounded">
<p className="text-red-700 text-sm">{message}</p>
</div>
)}
<button
type="submit"
className={`w-full p-3 rounded text-white font-medium transition
${status === 'loading'
? 'bg-blue-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'}`}
disabled={status === 'loading'}
>
{status === 'loading' ? 'Subscribing...' : 'Subscribe Now'}
</button>
<p className="text-xs text-gray-500 mt-2">
We respect your privacy and will never share your information.
</p>
</form>
)}
</div>
);
}
Best Practices for Next.js Forms
-
Use controlled components - Always manage your form state with React state for better control.
-
Avoid unnecessary re-renders - Use state efficiently and consider using libraries like
react-hook-form
for complex forms. -
Implement proper validation - Validate user input both on the client side and server side.
-
Show clear feedback - Provide visual feedback when users interact with your forms (errors, loading states, success messages).
-
Handle loading and error states - Always account for network requests taking time or failing.
-
Use semantic HTML - Use the appropriate form elements, labels, and ARIA attributes for accessibility.
-
Secure your forms - Implement CSRF protection and validate data on the server.
-
Optimize for mobile - Ensure your forms work well on all device sizes.
Summary
In this guide, we covered the fundamentals of working with forms in Next.js applications:
- Creating basic forms with React state
- Different form submission methods (client-side, server actions, API routes)
- Form validation techniques
- Building a practical, real-world newsletter subscription form
- Best practices for implementing forms in Next.js
Forms are a critical part of building interactive web applications, and Next.js provides several ways to handle form submissions efficiently. The approach you choose depends on your application's needs, but the principles of good form design remain consistent.
Additional Resources
- Next.js Documentation on Forms
- React Hook Form - A popular library for managing complex forms
- Zod - A TypeScript-first schema validation library
- Next.js Server Actions Documentation
Exercises
- Create a multi-step form wizard with form validation at each step
- Implement a file upload form using Next.js API routes
- Build a dynamic form with conditional fields that appear based on user selections
- Create a form with real-time validation feedback as the user types
- Implement a form that saves data to local storage so users can continue later
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)