Next.js Formik Integration
Forms are an essential part of web applications, but building them from scratch with proper validation can be challenging. In this tutorial, we'll explore how to integrate Formik—a popular form library for React—with your Next.js applications to create powerful, user-friendly forms.
What is Formik?
Formik is a form library for React that helps you handle form state, validation, error handling, and submission. It significantly reduces the boilerplate code needed when working with forms in React applications, making your forms more maintainable and easier to develop.
Key benefits of Formik include:
- Managing form state
- Handling form submissions
- Form validation and error messages
- Built-in support for common input types
- Easy integration with validation libraries like Yup
Setting Up Formik in a Next.js Project
Step 1: Install Required Packages
First, let's install Formik and Yup (a schema validation library that works well with Formik):
npm install formik yup
# or with yarn
yarn add formik yup
Step 2: Create a Basic Form Component
Let's create a simple registration form to demonstrate Formik integration:
// components/RegistrationForm.js
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
// Define validation schema with Yup
const RegistrationSchema = Yup.object().shape({
name: Yup.string()
.min(2, 'Name is too short!')
.max(50, 'Name is too long!')
.required('Name is required'),
email: Yup.string()
.email('Invalid email address')
.required('Email is required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Password is required'),
});
const RegistrationForm = () => {
return (
<div className="form-container">
<h2>Register</h2>
<Formik
initialValues={{ name: '', email: '', password: '' }}
validationSchema={RegistrationSchema}
onSubmit={(values, { setSubmitting }) => {
// In a real application, you would handle the submission here
// For example, making an API request to register the user
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 500);
}}
>
{({ isSubmitting }) => (
<Form>
<div className="form-group">
<label htmlFor="name">Name</label>
<Field type="text" name="name" className="form-control" />
<ErrorMessage name="name" component="div" className="error" />
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<Field type="email" name="email" className="form-control" />
<ErrorMessage name="email" component="div" className="error" />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Field type="password" name="password" className="form-control" />
<ErrorMessage name="password" component="div" className="error" />
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Register'}
</button>
</Form>
)}
</Formik>
</div>
);
};
export default RegistrationForm;
Step 3: Add Basic Styles
Let's add some basic styles to make our form look better. Create a CSS file for your form or add these styles to your global CSS:
/* styles/Form.module.css */
.form-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 15px;
}
.form-control {
width: 100%;
padding: 8px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
}
.error {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:disabled {
background-color: #bdc3c7;
}
button:hover:not(:disabled) {
background-color: #2980b9;
}
Step 4: Use the Form in a Next.js Page
Now, let's integrate our form into a Next.js page:
// pages/register.js
import Head from 'next/head';
import RegistrationForm from '../components/RegistrationForm';
export default function Register() {
return (
<div className="container">
<Head>
<title>Register - My Next.js App</title>
<meta name="description" content="Registration page" />
</Head>
<main>
<h1>Create an Account</h1>
<RegistrationForm />
</main>
</div>
);
}
Advanced Formik Features
Now that we understand the basics, let's explore some advanced Formik features.
Custom Form Controls
You can create custom form controls to maintain consistency in your application:
// components/FormikControls.js
import React from 'react';
import { Field, ErrorMessage } from 'formik';
export const TextInput = ({ label, name, ...rest }) => {
return (
<div className="form-group">
<label htmlFor={name}>{label}</label>
<Field id={name} name={name} className="form-control" {...rest} />
<ErrorMessage name={name} component="div" className="error" />
</div>
);
};
export const SelectField = ({ label, name, options, ...rest }) => {
return (
<div className="form-group">
<label htmlFor={name}>{label}</label>
<Field as="select" id={name} name={name} className="form-control" {...rest}>
<option value="">Select an option</option>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Field>
<ErrorMessage name={name} component="div" className="error" />
</div>
);
};
export const CheckboxGroup = ({ label, name, options, ...rest }) => {
return (
<div className="form-group">
<label>{label}</label>
<Field name={name}>
{({ field }) => {
return options.map(option => (
<div key={option.value} className="checkbox-item">
<input
type="checkbox"
id={`${name}-${option.value}`}
{...field}
value={option.value}
checked={field.value.includes(option.value)}
{...rest}
/>
<label htmlFor={`${name}-${option.value}`}>{option.label}</label>
</div>
));
}}
</Field>
<ErrorMessage name={name} component="div" className="error" />
</div>
);
};
Using Custom Components in a Form
Now let's use these custom components in a more complex form:
// components/ProfileForm.js
import React from 'react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import { TextInput, SelectField, CheckboxGroup } from './FormikControls';
const ProfileSchema = Yup.object().shape({
fullName: Yup.string().required('Full name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
occupation: Yup.string().required('Please select an occupation'),
interests: Yup.array().min(1, 'Select at least one interest')
});
const ProfileForm = () => {
const occupationOptions = [
{ value: 'developer', label: 'Developer' },
{ value: 'designer', label: 'Designer' },
{ value: 'manager', label: 'Manager' },
{ value: 'student', label: 'Student' },
];
const interestsOptions = [
{ value: 'programming', label: 'Programming' },
{ value: 'design', label: 'Design' },
{ value: 'business', label: 'Business' },
{ value: 'education', label: 'Education' },
];
return (
<div className="form-container">
<h2>Create Your Profile</h2>
<Formik
initialValues={{
fullName: '',
email: '',
occupation: '',
interests: []
}}
validationSchema={ProfileSchema}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
console.log(values);
alert('Form submitted successfully!');
setSubmitting(false);
}, 500);
}}
>
{({ isSubmitting }) => (
<Form>
<TextInput
label="Full Name"
name="fullName"
type="text"
/>
<TextInput
label="Email Address"
name="email"
type="email"
/>
<SelectField
label="Occupation"
name="occupation"
options={occupationOptions}
/>
<CheckboxGroup
label="Interests"
name="interests"
options={interestsOptions}
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</Form>
)}
</Formik>
</div>
);
};
export default ProfileForm;
Handling Form Submission with Next.js API Routes
In real-world applications, you'll want to submit form data to your server. Next.js API routes are perfect for this:
Step 1: Create an API Route
// pages/api/register.js
export default async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
try {
const { name, email, password } = req.body;
// Here you would typically:
// 1. Validate the data
// 2. Hash the password
// 3. Store in database
// For this example, we'll just mock a response
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 1000));
// Return success response
res.status(200).json({
success: true,
message: 'Registration successful',
user: { name, email, id: Math.random().toString(36).substring(7) }
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'An error occurred during registration'
});
}
}
Step 2: Update Form Submission to Use the API
// components/RegistrationForm.js (updated)
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { useRouter } from 'next/router';
// ... validation schema code ...
const RegistrationForm = () => {
const router = useRouter();
return (
<div className="form-container">
<h2>Register</h2>
<Formik
initialValues={{ name: '', email: '', password: '' }}
validationSchema={RegistrationSchema}
onSubmit={async (values, { setSubmitting, setStatus }) => {
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
});
const data = await response.json();
if (data.success) {
// Redirect on successful registration
router.push('/registration-success');
} else {
// Show error message
setStatus({ error: data.message });
}
} catch (error) {
setStatus({ error: 'An error occurred. Please try again.' });
} finally {
setSubmitting(false);
}
}}
>
{({ isSubmitting, status }) => (
<Form>
{/* Show any submission errors */}
{status && status.error && (
<div className="error-message">{status.error}</div>
)}
{/* ... form fields ... */}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</Form>
)}
</Formik>
</div>
);
};
export default RegistrationForm;
Formik with TypeScript in Next.js
If you're using TypeScript with your Next.js project, you can add strong typing to your Formik forms:
// components/LoginForm.tsx
import React from 'react';
import { Formik, Form, Field, ErrorMessage, FormikHelpers } from 'formik';
import * as Yup from 'yup';
// Define the form values interface
interface LoginFormValues {
email: string;
password: string;
rememberMe: boolean;
}
// Define the validation schema
const LoginSchema = Yup.object().shape({
email: Yup.string()
.email('Invalid email address')
.required('Email is required'),
password: Yup.string()
.required('Password is required'),
rememberMe: Yup.boolean(),
});
const LoginForm: React.FC = () => {
// Define initial form values
const initialValues: LoginFormValues = {
email: '',
password: '',
rememberMe: false,
};
// Handle form submission
const handleSubmit = async (
values: LoginFormValues,
{ setSubmitting }: FormikHelpers<LoginFormValues>
) => {
try {
// Submit form data to your API
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
const data = await response.json();
console.log('Login response:', data);
// Handle response accordingly
} catch (error) {
console.error('Login error:', error);
} finally {
setSubmitting(false);
}
};
return (
<div className="form-container">
<h2>Login</h2>
<Formik
initialValues={initialValues}
validationSchema={LoginSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting }) => (
<Form>
<div className="form-group">
<label htmlFor="email">Email</label>
<Field type="email" name="email" className="form-control" />
<ErrorMessage name="email" component="div" className="error" />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Field type="password" name="password" className="form-control" />
<ErrorMessage name="password" component="div" className="error" />
</div>
<div className="form-group checkbox">
<label>
<Field type="checkbox" name="rememberMe" />
Remember me
</label>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</Form>
)}
</Formik>
</div>
);
};
export default LoginForm;
Best Practices for Formik with Next.js
- Form Organization: Split large forms into smaller components
- Reusable Components: Create reusable form controls to maintain consistency
- Server-Side Validation: Always validate form data on the server as well
- Error Handling: Provide clear error messages for both field validation and API errors
- Loading States: Show loading indicators during form submission
- Form Reset: Reset forms after successful submission when appropriate
- Accessibility: Ensure your forms are accessible with proper labels and ARIA attributes
Summary
In this tutorial, we've covered how to integrate Formik with Next.js applications to create powerful, user-friendly forms. We learned:
- Setting up Formik in a Next.js project
- Creating basic forms with validation using Yup
- Building custom form components for reusability
- Handling form submission with Next.js API routes
- Using Formik with TypeScript for type safety
- Following best practices for form development
Formik makes handling forms in Next.js significantly easier by managing form state, validation, and submission in a clean and organized way. By combining Formik with Next.js's powerful features, you can create dynamic forms that provide excellent user experiences.
Additional Resources
- Formik Official Documentation
- Yup Schema Validation
- Next.js API Routes Documentation
- Web Accessibility Initiative - Forms
Exercises
- Create a multi-step form wizard using Formik and Next.js
- Build a dynamic form where fields change based on previous selections
- Implement a form with file uploads using Formik and Next.js API routes
- Add client-side form analytics to track form completion rates
- Create a form with real-time validation as the user types
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)