In the ever-evolving landscape of web development, creating dynamic and user-friendly forms is a fundamental skill. Forms are the gateways through which users interact with your application, whether it’s for submitting feedback, creating accounts, or making purchases. A poorly designed form can lead to frustration, data entry errors, and a negative user experience. This is where robust form management libraries like Formik and validation schemas like Yup come into play. This tutorial will guide you through building dynamic forms in Next.js, leveraging the power of Formik for form state management and Yup for schema-based validation. We’ll explore practical examples, common pitfalls, and best practices to help you create efficient and user-friendly forms.
Why Dynamic Forms Matter
Dynamic forms are forms that adapt to user input or other conditions. For example, a form that reveals additional fields based on a user’s selection from a dropdown menu is a dynamic form. These forms offer a more personalized and streamlined user experience. They can also significantly improve data quality by validating input in real-time and preventing errors before they occur.
Prerequisites
Before diving into the code, make sure you have the following:
- A basic understanding of React and JavaScript.
- Node.js and npm (or yarn) installed on your system.
- A Next.js project set up. If you don’t have one, you can create a new project using:
npx create-next-app my-form-app - Familiarity with the command line interface (CLI).
Setting Up the Project and Installing Dependencies
First, navigate to your Next.js project directory. Then, install Formik and Yup using npm or yarn:
npm install formik yup
# or
yarn add formik yup
Understanding Formik and Yup
Formik
Formik is a popular library for building forms in React. It simplifies the process of managing form state, handling form submissions, and dealing with validation. Key features of Formik include:
- **State Management:** Formik manages the form’s values, errors, and submission status.
- **Submission Handling:** It provides a clean way to handle form submissions, including asynchronous operations.
- **Validation:** Integrates well with validation libraries like Yup.
- **Performance:** Optimized for performance, especially with complex forms.
Yup
Yup is a JavaScript schema builder for value parsing and validation. It allows you to define a schema for your form data, specifying the expected data types, required fields, and custom validation rules. Key features of Yup include:
- **Schema Definition:** Defines a clear structure for your form data.
- **Validation Rules:** Supports a wide range of validation rules (required, email, min/max length, etc.).
- **Custom Validation:** Allows you to create your own validation rules.
- **Error Handling:** Provides detailed error messages for easy debugging.
Building a Simple Form with Formik and Yup
Let’s start with a simple contact form. Create a new file, such as ContactForm.js, in your components directory. This form will have fields for name, email, and a message.
// components/ContactForm.js
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const ContactForm = () => {
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
message: Yup.string().required('Message is required'),
});
const handleSubmit = (values, { setSubmitting, resetForm }) => {
// Simulate an API call
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
resetForm();
}, 400);
};
return (
{({ isSubmitting }) => (
<div>
<label>Name:</label>
</div>
<div>
<label>Email:</label>
</div>
<div>
<label>Message:</label>
</div>
<button type="submit" disabled="{isSubmitting}">
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
);
};
export default ContactForm;
Let’s break down this code:
- **Imports:** We import necessary components from Formik and Yup.
- **Validation Schema:** We define a Yup schema that specifies the validation rules for each field (name, email, message).
- **handleSubmit:** This function is called when the form is submitted. In a real-world scenario, you would make an API call here.
- **Formik Component:** We wrap our form with the
Formikcomponent. We passinitialValues,validationSchema, andonSubmitprops to configure the form. - **Form, Field, and ErrorMessage:** These are Formik’s components for rendering the form, its fields, and displaying validation errors.
- **isSubmitting:** A boolean value managed by Formik that indicates if the form is currently being submitted.
To use this form, import it into your page component (e.g., pages/index.js):
// pages/index.js
import React from 'react';
import ContactForm from '../components/ContactForm';
const Home = () => {
return (
<div>
<h1>Contact Us</h1>
</div>
);
};
export default Home;
Adding Conditional Fields
Now, let’s make our form dynamic. Suppose we want to add a field for “reason for contact” and show a different set of fields depending on the selected reason. We’ll add a select dropdown for the “reason” and use the selected value to conditionally render other fields.
Modify your ContactForm.js file as follows:
// components/ContactForm.js
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const ContactForm = () => {
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
reason: Yup.string().required('Please select a reason'),
message: Yup.string().when('reason', {
is: (value) => value !== '', // Ensure message is required when a reason is selected
then: Yup.string().required('Message is required'),
otherwise: Yup.string(),
}),
subject: Yup.string().when('reason', {
is: 'other',
then: Yup.string().required('Subject is required'),
otherwise: Yup.string(),
}),
});
const handleSubmit = (values, { setSubmitting, resetForm }) => {
// Simulate an API call
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
resetForm();
}, 400);
};
return (
{({ isSubmitting, values }) => (
<div>
<label>Name:</label>
</div>
<div>
<label>Email:</label>
</div>
<div>
<label>Reason for Contact:</label>
Select a reason
Support
Feedback
Other
</div>
{values.reason === 'other' && (
<div>
<label>Subject:</label>
</div>
)}
<div>
<label>Message:</label>
</div>
<button type="submit" disabled="{isSubmitting}">
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
);
};
export default ContactForm;
Here’s what changed:
- We added a
reasonfield with a select dropdown. - We updated the Yup schema to include validation for the
reasonfield. - We used Yup’s
.when()method to conditionally validate thesubjectfield based on the selected reason. - We conditionally rendered the
subjectfield based on thevalues.reason. - We added a conditional requirement for the message field, it is now required if a reason is selected.
Advanced Validation and Custom Validation
Yup offers powerful features for advanced validation. Let’s explore how to create a custom validation function. Suppose we want to validate that the email domain is one we accept. Add the following to your ContactForm.js:
// components/ContactForm.js
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const ContactForm = () => {
const allowedDomains = ['@example.com', '@test.com'];
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
email: Yup.string()
.email('Invalid email')
.required('Email is required')
.test('domain', 'Invalid domain', (value) => {
if (!value) return true; // Allow if the field is not filled
const domain = value.substring(value.indexOf('@'));
return allowedDomains.includes(domain);
}),
reason: Yup.string().required('Please select a reason'),
message: Yup.string().when('reason', {
is: (value) => value !== '',
then: Yup.string().required('Message is required'),
otherwise: Yup.string(),
}),
subject: Yup.string().when('reason', {
is: 'other',
then: Yup.string().required('Subject is required'),
otherwise: Yup.string(),
}),
});
const handleSubmit = (values, { setSubmitting, resetForm }) => {
// Simulate an API call
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
resetForm();
}, 400);
};
return (
{({ isSubmitting, values }) => (
<div>
<label>Name:</label>
</div>
<div>
<label>Email:</label>
</div>
<div>
<label>Reason for Contact:</label>
Select a reason
Support
Feedback
Other
</div>
{values.reason === 'other' && (
<div>
<label>Subject:</label>
</div>
)}
<div>
<label>Message:</label>
</div>
<button type="submit" disabled="{isSubmitting}">
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
);
};
export default ContactForm;
Here, we added the following:
- An
allowedDomainsarray. - We used the
.test()method in the Yup schema to create a custom validation rule for the email domain. - The custom validation function extracts the domain from the email and checks if it’s in the
allowedDomainsarray.
Handling Form Submission and Error Messages
Let’s focus on error handling and form submission. In the previous examples, we used a simple alert to display the form data. In a real application, you would make an API call to submit the form data to your backend. Additionally, you’ll want to display error messages to the user if the submission fails.
Modify your ContactForm.js file as follows:
// components/ContactForm.js
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const ContactForm = () => {
const allowedDomains = ['@example.com', '@test.com'];
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
email: Yup.string()
.email('Invalid email')
.required('Email is required')
.test('domain', 'Invalid domain', (value) => {
if (!value) return true; // Allow if the field is not filled
const domain = value.substring(value.indexOf('@'));
return allowedDomains.includes(domain);
}),
reason: Yup.string().required('Please select a reason'),
message: Yup.string().when('reason', {
is: (value) => value !== '',
then: Yup.string().required('Message is required'),
otherwise: Yup.string(),
}),
subject: Yup.string().when('reason', {
is: 'other',
then: Yup.string().required('Subject is required'),
otherwise: Yup.string(),
}),
});
const handleSubmit = async (values, { setSubmitting, setErrors, resetForm }) => {
try {
// Simulate an API call
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Form data:', values);
// In a real application, you would make an API call here.
resetForm();
alert('Form submitted successfully!');
} catch (error) {
// Handle errors from the API call
console.error('Submission error:', error);
setErrors({ submit: 'An error occurred during submission. Please try again.' });
}
setSubmitting(false);
};
return (
{({ isSubmitting, values, errors }) => (
<div>
<label>Name:</label>
</div>
<div>
<label>Email:</label>
</div>
<div>
<label>Reason for Contact:</label>
Select a reason
Support
Feedback
Other
</div>
{values.reason === 'other' && (
<div>
<label>Subject:</label>
</div>
)}
<div>
<label>Message:</label>
</div>
{errors.submit && <div>{errors.submit}</div>}
<button type="submit" disabled="{isSubmitting}">
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
);
};
export default ContactForm;
Here, we updated the handleSubmit function:
- We added an
asyncfunction and usedawaitto simulate an API call withsetTimeout. - We added a
try...catchblock to handle potential errors during the API call. - If the API call succeeds, we reset the form using
resetForm()and show a success message. - If the API call fails, we set an error message using
setErrors({ submit: '...' }). - We render the submit error in the form.
Styling Your Forms
While Formik and Yup handle form logic and validation, you’ll need to style your forms to make them visually appealing. You can use CSS, CSS-in-JS libraries (like Styled Components or Emotion), or UI component libraries (like Material UI or Ant Design) for styling. Here’s a basic example using inline styles:
// components/ContactForm.js
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const ContactForm = () => {
const allowedDomains = ['@example.com', '@test.com'];
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
email: Yup.string()
.email('Invalid email')
.required('Email is required')
.test('domain', 'Invalid domain', (value) => {
if (!value) return true; // Allow if the field is not filled
const domain = value.substring(value.indexOf('@'));
return allowedDomains.includes(domain);
}),
reason: Yup.string().required('Please select a reason'),
message: Yup.string().when('reason', {
is: (value) => value !== '',
then: Yup.string().required('Message is required'),
otherwise: Yup.string(),
}),
subject: Yup.string().when('reason', {
is: 'other',
then: Yup.string().required('Subject is required'),
otherwise: Yup.string(),
}),
});
const handleSubmit = async (values, { setSubmitting, setErrors, resetForm }) => {
try {
// Simulate an API call
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Form data:', values);
// In a real application, you would make an API call here.
resetForm();
alert('Form submitted successfully!');
} catch (error) {
// Handle errors from the API call
console.error('Submission error:', error);
setErrors({ submit: 'An error occurred during submission. Please try again.' });
}
setSubmitting(false);
};
return (
{({ isSubmitting, values, errors }) => (
<div style="{{">
<label style="{{">Name:</label>
</div>
<div style="{{">
<label style="{{">Email:</label>
</div>
<div style="{{">
<label style="{{">Reason for Contact:</label>
Select a reason
Support
Feedback
Other
</div>
{values.reason === 'other' && (
<div style="{{">
<label style="{{">Subject:</label>
</div>
)}
<div style="{{">
<label style="{{">Message:</label>
</div>
{errors.submit && <div style="{{">{errors.submit}</div>}
<button type="submit" disabled="{isSubmitting}" style="{{">
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
);
};
export default ContactForm;
In this example, we’ve added inline styles to the form, labels, fields, and button. While inline styles are convenient for quick styling, consider using CSS modules, styled components, or a UI library for more complex and maintainable styling.
Common Mistakes and How to Fix Them
- **Incorrect Field Names:** Ensure that the
nameattributes of yourFieldcomponents match the keys in yourinitialValuesand your Yup schema. - **Missing ErrorMessage Components:** Always include
ErrorMessagecomponents to display validation errors. - **Incorrect Validation Schema:** Double-check your Yup schema for any typos or incorrect validation rules.
- **Not Handling Submission Errors:** Make sure you handle potential errors during form submission and display appropriate error messages to the user.
- **Forgetting to Set setSubmitting(false):** Always set
setSubmitting(false)after form submission (whether successful or not) to re-enable the submit button.
Key Takeaways
- Formik simplifies form state management and submission handling.
- Yup enables you to define and validate your form data with a clear schema.
- Dynamic forms can improve the user experience and data quality.
- Always handle form submission errors gracefully.
- Style your forms to enhance the user interface.
FAQ
- Can I use Formik and Yup with other UI libraries like Material UI or Ant Design?
Yes, Formik integrates well with various UI libraries. You can wrap your UI components with Formik’sFieldcomponent and use the library’s features for form management and validation. - How do I handle file uploads with Formik?
Formik doesn’t handle file uploads directly. You’ll need to use theonChangeevent on the file input field to update the form values with the selected files. You may also need a separate library or API to upload the files to a server. - How can I validate a form on blur (as the user leaves a field)?
Formik provides thevalidateOnBlurprop. Set it totruein yourFormikcomponent. This will trigger validation whenever a field loses focus. - How do I reset a form after submission?
You can use theresetForm()function provided by Formik within youronSubmithandler. This resets the form to its initial values.
By combining Formik and Yup, you can create robust, dynamic forms that are both user-friendly and reliable. Remember to focus on clear error messages, a smooth user experience, and proper validation to ensure data quality and user satisfaction. With the knowledge gained from this guide, you should now be well-equipped to build sophisticated forms in your Next.js applications that meet the needs of your users. The journey of web development is one of continuous learning, and mastering form management is a valuable skill in your toolkit. Continue to experiment, iterate, and refine your approach to build forms that are not just functional, but also a pleasure to use.
