Handling Complex Multi Step Forms with Formik and Yup
Introduction:
When creating forms on our own, we are tasked with managing our forms values, writing our custom event handlers for every input, handling error states manually and the worst part is creating verbose validation logic. This might be manageable for simple forms, but for more complex forms, it becomes imperative to use a form handler.
I love Formik and Yup because they're simple to use while still being lightweight so that they don't affect the performance of our site.
Formik provides a minimal API to manage form state, validation, error handling, and form submission. It gives you full control over how your form behaves without being too restrictive as it gives you the room and autonomy to adjust and manipulate your form to your desired state. Meanwhile, Yup is a concise but powerful object schema validation that compliments Formik to create powerful validation schemas, ensuring that your forms have strong validation rules and it is small enough for the browser and fast enough for runtime usage so not taking up too much performance space (which is very important for external apps).
Formik has a special config option / prop for Yup called validationSchema which will automatically transform Yup's validation errors into a pretty object whose keys match values and touched i.e whatever name you have for your field values and initialValues should match the name provided to the validationSchema.
What I love about Formik is that it provides components that abstracts and hides unneeded info under the hood by offering components like <Form />, <Field />, and <ErrorMessage />
that handle value changes, blur events, and validation messages, reducing the need for repetitive code. These components use React context, allowing you to access form state globally and make managing complex forms more efficient.
Common scenarios where complex forms are necessary include checkout processes, or applications with dynamic user data, such as adding multiple addresses or phone numbers.
Project Overview:
In this article, we'll build a multi-step form for a job board, where each step gathers specific information, such as job details and company information. We'll also implement dynamic fields, error handling, and validations using Yup and this is going to be done in a simple and succinct way.
Setup for the Tutorial
We'll start by setting up a React project with Formik and Yup. The installation is straightforward:
Formik Installation: npm install formik - save or yarn add formik
Yup Installation: npm install yup - save or yarn add yup
Once installed, we'll configure Formik and Yup, ensuring the form values, validation rules, and submission process are integrated.
Basic Setup of Formik
import { Formik, Form, Field, ErrorMessage } from "formik";
const Basic = () => (
<div>
<Formik
initialValues={{ email: "", password: "" }} // Initial form values
validate={(values) => {
// Custom validation logic
const errors = {};
if (!values.email) {
errors.email = "Required";
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = "Invalid email address";
}
if (!values.password) {
errors.password = "Required";
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
// Handle form submission
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({ isSubmitting }) => (
<Form>
<Field type="email" name="email" /> {/* Email input field */}
<ErrorMessage name="email" component="div" /> {/* Error for email */}
<Field type="password" name="password" /> {/* Password input field */}
<ErrorMessage name="password" component="div" />{" "}
{/* Error for password */}
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</Form>
)}
</Formik>
</div>
);
export default Basic;
Initial Values: You define the initial state of your form by passing an initialValues object to Formik. In this case, the form has two fields: email and password.
Validation: You can create custom validation logic in the validate function. Here, the form checks if the email and password fields are empty or if the email is in an invalid format.
Submission Handling: When the form is submitted, the onSubmit function handles the form data. This function simulates a form submission by displaying the values in an alert after a 400ms delay.
Field: This component connects your input field to Formik, automatically managing value changes and error handling for you. You simply should add the type if it's not a normal input field and the name (this should tally with the name on the initialValues and validation schema.
ErrorMessage: Displays error messages for each field, simplifying the error display logic, this should contain the name of the field.
STEP 1: MultiStepForm Component
Create a Multi-Step-Form component file within your components folder.
project/src/components/multi-step-form.tsx
import { Fragment } from "react";
type stepProps = {
steps: any[];
currentStep: number;
};
const MultiStepForm = ({
steps,
currentStep,
}:
stepProps) => {
return (
<div className="md:max-w-5xl mx-auto p-4 text-black font-base">
<div className="md:flex items-center justify-between mb-8 hidden">
{steps.map((step, index) => (
<Fragment key={step.id}>
<div className="flex items-center mx-4">
<div
className={`rounded-full h-12 w-12 flex items-center justify-center text-white ${
currentStep >= step.id ? "bg-purple-200" : "bg-gray-400"
}`}
>
{step.id}
</div>
<div className="ml-2 text-xl font-medium ">{step.title}</div>
</div>
{index < steps.length - 1 && (
<div
className={`flex-1 h-1 ${
currentStep > step.id ? "bg-purple-200" : "bg-gray-400"
}`}
></div>
)}
</Fragment>
))}
</div>
<div className="mb-4">
<h2 className="text-2xl font-bold mb-4 flex justify-center">
{steps[currentStep - 1].title}
</h2>
</div>
</div>
);
};
export default MultiStepForm;
The MultiStepForm component displays a progress bar showing the current step in a multi-step form process. Each step is represented by a number and title, with lines connecting them for completed steps.
Steps Array: The steps array contains an ID and title for each step. Current Step: The currentStep prop controls the active step visually and in the title. Step Indicator: The round indicators change color when completed, and a horizontal line connects the steps.
STEP 2: Setting Up AddJob Component
Next, we create the AddJob component, which will hold the multi-step form's logic.
Within your SRC routes structure create a page for add-job.tsx, let's start with the boilerplate.
Here, currentStep tracks the user's position in the form, and goToNextStep and goToPreviousStep manages navigation by checking what step the user is on, add one step to the step to take the user to the next page or minus one step to take the user back.
import { useState } from "react";
const AddJob = () => {
const [currentStep, setCurrentStep] = useState(1);
const steps = [
{ id: 1, title: "Job Information" },
{ id: 2, title: "Company Information" },
{ id: 3, title: "Submit" },
];
const goToNextStep = () => {
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const goToPreviousStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
};
return (TODO)
export default AddJob;
STEP 3: Form Validation with Yup
We are going to define our validation schema using Yup.
import * as Yup from "yup"
const validationSchema = Yup.object({
jobTitle: Yup.string().required("Job title is required"),
jobDescription: Yup.string().required("Please describe this role"),
jobCategory: Yup.string().required("Kindly select a job category"),
jobSkills: Yup.string().required("Please select skills needed"),
jobSalary: Yup.string().required("Select a job salary"),
jobUrl: Yup.string().required("Provide the link for the job"),
companyName: Yup.string().required("Please provide the name"),
companyHq: Yup.string(),
companysWebsite: Yup.string().required("Enter the company's website"),
companysEmail: Yup.string()
.email("Invalid email address")
.required("Enter the company's email"),
companysDescription: Yup.string().required("What does the company do?"),
});
Currently, we are doing basic schema checks for almost all fields seen above.
Yup.string()/required("Error message to be shown to the user") states that the field is a required field which expects string values.
While companysEmail: Yup.string().email("Invalid email address")required("Enter the company's email"), validates that the user has inputted a string with an email format and it is a required field.
While there are more complex schema validation and I might create a separate article delving into it, let's keep the validation schema simple for our use case.
Step 4: Integrating Formik for Form Handling
In this step, we'll integrate Formik to handle form data, validation, and submission logic. We'll also connect it to our multi-step form navigation.
Importing Formik and Setting Up Form Structure
First, let's import Formik, Field, Form, and ErrorMessage components. We'll then set up our form structure with initial values and link it to our validation schema.
Now, let's add the Formik component and bind it with our initial form values, validation schema, and the submission handler. We also include the MultiStepForm component to visually display the current step.
import { useState } from "react";
import MultiStepForm from "./components/multi-step-form";
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";
const validationSchema = Yup.object({
jobTitle: Yup.string().required("Job title is required"),
// other fields...
});
const AddJob = () => {
const [currentStep, setCurrentStep] = useState(1);
const steps = [
…
];
const goToNextStep = () => {
…
};
const goToPreviousStep = () => {
…
};
const createJobFn = (values, { resetForm, setSubmitting }) => {
console.log(values);
resetForm();
setSubmitting(false);
};
return (
<section className="w-full nx-auto flex-row min-h-screen flex justify-center items-center bg-gray-100 px-4">
<div className="bg-white shadow-lg rounded-lg p-8">
<MultiStepForm
steps={steps}
currentStep={currentStep}
/>
<Formik
initialValues={{
jobTitle: "",
jobDescription: "",
jobCategory: "",
jobSkills: "",
jobSalary: "",
jobUrl: "",
companyName: "",
companyHq: "",
companysWebsite: "",
companysEmail: "",
companysDescription: "",
}}
validationSchema={validationSchema}
onSubmit={(values, { resetForm, setSubmitting }) => {
createJobFn(values, {
setSubmitting,
resetForm,
});
}}
>
{({
isSubmitting,
setFieldValue,
isValid,
}) => <div className="md:container md:mx-auto mb-12">TODO</div>}
</Formik>
</div>
</section>
);
};
export default AddJob;
Initial Values: We define the initial form values using initialValues in the Formik component. Each field corresponds to the keys in our validation schema. These values will change as the user inputs data into the form.
Validation: The validationSchema prop connects Formik with the Yup validation we set up earlier. This ensures that when the form is submitted, the data is validated according to the rules specified (e.g., required fields).
Form Submission: The onSubmit prop defines the createJobFn function to handle the form submission. It logs the form values to the console, resets the form, and sets isSubmitting to false to prevent multiple submissions.
Form State Handling: Formik provides useful props (values, errors, touched, isSubmitting, isValid, etc.) to manage and track form states. These can be used to conditionally render content, control button states, and handle form validation and submission more dynamically. Here, we are passing isSubmitting and isValid that we would use in the following steps to check if the user has filled the required fields before they can submit the form - this shows how using Formik gives us the power to control and manage our forms.
Step 5: Constant File For Our Select Fields
Create a constant file that we would map through to get our select options as some of our fields would be a select field.
export const CATEGORIES = [
{ id: "1", name: "Engineering" },
{ id: "2", name: "Marketing" },
{ id: "3", name: "Design" },
];
export const SKILLS = [
{ id: "1", name: "JavaScript" },
{ id: "2", name: "React" },
{ id: "3", name: "CSS" },
];
Step 6: Return Statement
..
return (
<section className="w-full nx-auto flex-row min-h-screen flex justify-center items-center bg-gray-100 px-4">
<div className="bg-white shadow-lg rounded-lg p-8">
<MultiStepForm steps={steps} currentStep={currentStep} />
<Formik
initialValues={{
jobTitle: "",
…other fields
}}
validationSchema={validationSchema}
onSubmit={(values, { resetForm, setSubmitting }) => {
…other fields
});
}}
>
{({ isSubmitting, isValid }) => (
<div>
<Form className="bg-white shadow-lg rounded-lg w-full px-8 max-w-4xl mx-auto">
{currentStep === 1 && (
<div className="flex flex-col mb-2">
<div className="mb-4">
<label
htmlFor="jobTitle"
className="block text-gray-700 text-sm font-bold mb-2"
>
Job Title{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobTitle"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobTitle"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="flex flex-wrap">
<div className="w-full md:w-1/2 md:pr-5 mb-2">
<label
htmlFor="jobCategory"
className="block text-gray-700 text-sm font-bold"
>
Job Category{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<div className="relative">
<Field
as="select"
name="jobCategory"
className="shadow appearance-none capitalize border rounded w-full py-2 px-3 bg-white text-black text-sm leading-tight focus:outline-none focus:shadow-outline"
>
<option value="" disabled>
Select
</option>
{CATEGORIES?.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</Field>
</div>
<ErrorMessage
name="jobCategory"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="w-full md:w-1/2 md:pl-5 mb-2">
<label
htmlFor="jobSkills"
className="block text-gray-700 text-sm font-bold"
>
Job Skills{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<div className="relative">
<Field
as="select"
name="jobSkills"
className="shadow appearance-none capitalize border rounded w-full py-2 px-3 bg-white text-black text-sm leading-tight focus:outline-none focus:shadow-outline"
>
<option value="" disabled>
Select
</option>
{SKILLS?.map((skill) => (
<option key={skill.id} value={skill.id}>
{skill.name}
</option>
))}
</Field>
</div>
<ErrorMessage
name="jobSkills"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
<div className="flex flex-wrap">
<div className="w-full md:w-1/2 md:pr-5 mb-2">
<label
htmlFor="jobSalary"
className="block text-gray-700 text-sm font-bold"
>
Job Salary{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobSalary"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobSalary"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>{" "}
</div>
<div className="w-full md:w-1/2 md:pl-5 mb-2">
<label
htmlFor="jobUrl"
className="block text-gray-700 text-sm font-bold"
>
Job Link{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobUrl"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobUrl"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
<div className="mb-2">
<label
htmlFor="jobDescription"
className="block text-gray-700 text-sm font-bold"
>
Job Description{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobDescription"
type="textarea"
as="textarea"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobDescription"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
)}
{currentStep === 2 && (
<div>
<div className="mb-2">
<label
htmlFor="companyName"
className="block text-gray-700 text-sm font-bold"
>
Company Name{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companyName"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 capitalize bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companyName"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companyHq"
className="block text-gray-700 text-sm font-bold"
>
Company HQ
</label>
<Field
name="companyHq"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 capitalize bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companyHq"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companysWebsite"
className="block text-gray-700 text-sm font-bold"
>
Company's Website{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companysWebsite"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companysWebsite"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companysEmail"
className="block text-gray-700 text-sm font-bold"
>
Company's Email{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companysEmail"
type="email"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companysEmail"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companysDescription"
className="block text-gray-700 text-sm font-bold"
>
Company's Description{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companysDescription"
type="textarea"
as="textarea"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companysDescription"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
)}
{currentStep === 3 && (
<div className="flex flex-col justify-center text-black">
<h1>Thank You.</h1>
<h2>The team would review and provide feedback</h2>
</div>
)}
<div className="flex justify-between p-4">
{currentStep === 1 ? (
<div></div>
) : (
<button
onClick={goToPreviousStep}
type="button"
className="px-6 py-2 min-w-[120px] text-center text-white bg-purple-900 border border-purple-900 rounded active:text-purple-200 hover:bg-transparent hover:text-purple-900 focus:outline-none"
>
Previous
</button>
)}
{currentStep === 1 || currentStep === 2 ? (
<button
onClick={(e) => {
e.preventDefault();
goToNextStep();
}}
type="button"
className="px-6 py-2 min-w-[120px] text-center text-white bg-purple-900 border border-purple-900 rounded active:text-purple-200 hover:bg-transparent hover:text-purple-900 focus:outline-none"
>
Next
</button>
) : (
<button
type="submit"
disabled={isSubmitting || !isValid}
className="px-6 py-2 min-w-[120px] text-center text-white bg-purple-900 border border-purple-900 rounded active:text-purple-200 hover:bg-transparent hover:text-purple-900 focus:outline-none"
>
{isValid ? "Incomplete form" : "Submit"}
</button>
)}
</div>
</Form>
</div>
)}
</Formik>
</div>
</section>
);
};
export default AddJob;
Let's break down the key parts of this return statement, focusing on currentStep === 1, labels, fields, error messages, and the button section.
currentStep === 1:
This block renders the form fields for step 1. The currentStep prop controls which part of the form is shown. When currentStep === 1, it displays input fields like jobTitle, jobCategory, jobSkills, jobSalary, jobUrl, and jobDescription. This structure allows for multi-step forms, showing different fields depending on the current step of the form.
Labels, Fields, and Error Messages:
Labels: Each form field includes a element to describe the input. This label is connected to the corresponding field using the htmlFor attribute. For example:
<label htmlFor="jobTitle" className="block text-gray-700 text-sm font-bold mb-2">
Job Title <span className="text-red-500">*</span>
</label>
Fields: Uses the Field component from Formik to create input fields. The name prop links the field to a property in Formik's initialValues. This connection enables Formik to manage the form's state and validation.
<Field
name="jobTitle"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm"
/>
Error Messages: The ErrorMessage component is used to display validation errors. name specifies which field's error to display, and component defines the HTML element to render the error message in and then a red text styling.
<ErrorMessage
name="jobTitle"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
Button Explanation: There are three buttons in the form, shown conditionally based on the currentStep.
Previous Button Displayed only when currentStep is not 1. This button allows the user to go back to the previous step:
<button
onClick={goToPreviousStep}
type="button"
className="…"
>
Previous
</button>
Next Button: Displayed for steps 1 and 2. This button advances the form to the next step:
<button
onClick={(e) => {
e.preventDefault();
goToNextStep();
}}
type="button"
className="..."
>
Next
</button>
Submit Button: Displayed when the final step (not 1 or 2) is reached. The button's disabled state is controlled by Formik's isSubmitting and isValid properties so this help us disable the button if the form is already in the process of submission, this prevents duplicate submission or if its invalid i.e if the user does not enter the required fields in the valid format as defined in the validation schema.
Then our button text displays "Incomplete form" if the form is invalid, otherwise, it shows "Submit".
<button
type="submit"
disabled={isSubmitting || !isValid}
className="…"
>
{isValid ? "Incomplete form" : "Submit"}
</button>
That's it, here's the full working code. I hope this gives you some solid information as to how powerful Formik and Yup are in terms of giving you the control to build lightweight forms.
The documentation is very easy to browse through and understand so feel free to check out the documentation to learn more.
Here's the full AddJob Form code below;
import { useState } from "react";
import MultiStepForm from "./components/multi-step-form";
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";
import { CATEGORIES, SKILLS } from "./constants";
const validationSchema = Yup.object({
jobTitle: Yup.string().required("Job title is required"),
jobDescription: Yup.string().required("Please describe this role"),
jobCategory: Yup.string().required("Kindly select a job category"),
jobSkills: Yup.string().required("Please select skills needed"),
jobType: Yup.string().required("This is required"),
jobLocation: Yup.string().required("Please select relevant locations"),
jobLevel: Yup.string().required("What level is this job?"),
jobSalary: Yup.string().required("Select a job salary"),
jobUrl: Yup.string().required("Provide the link for the job"),
// createdBy: Yup.string(),
companyName: Yup.string().required("Please provide the name"),
companyHq: Yup.string(),
companysWebsite: Yup.string().required("Enter the company's website"),
companysEmail: Yup.string()
.email("Invalid email address")
.required("Enter the company's email"),
companysDescription: Yup.string().required("What does the company do?"),
});
const AddJob = () => {
const [currentStep, setCurrentStep] = useState(1);
const steps = [
{ id: 1, title: "Job Information" },
{ id: 2, title: "Company Information" },
{ id: 3, title: "Submit" },
];
const goToNextStep = () => {
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const goToPreviousStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const createJobFn = (values: any, { resetForm, setSubmitting }: any) => {
// Define your createJobFn logic here
console.log(values);
resetForm();
setSubmitting(false);
};
return (
<section className="w-full nx-auto flex-row min-h-screen flex justify-center items-center bg-gray-100 px-4">
<div className="bg-white shadow-lg rounded-lg p-8">
<MultiStepForm steps={steps} currentStep={currentStep} />
<Formik
initialValues={{
jobTitle: "",
jobDescription: "",
jobCategory: "",
jobSkills: "",
jobType: "",
jobLocation: "",
jobLevel: "",
jobSalary: "",
jobUrl: "",
// createdBy: "",
companyName: "",
companyHq: "",
companysWebsite: "",
companysEmail: "",
companysDescription: "",
}}
validationSchema={validationSchema}
onSubmit={(values, { resetForm, setSubmitting }) => {
createJobFn(values, {
setSubmitting,
resetForm,
});
}}
>
{({ isSubmitting, isValid }) => (
<div>
<Form className="bg-white shadow-lg rounded-lg w-full px-8 max-w-4xl mx-auto">
{currentStep === 1 && (
<div className="flex flex-col mb-2">
<div className="mb-4">
<label
htmlFor="jobTitle"
className="block text-gray-700 text-sm font-bold mb-2"
>
Job Title{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobTitle"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobTitle"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="flex flex-wrap">
<div className="w-full md:w-1/2 md:pr-5 mb-2">
<label
htmlFor="jobCategory"
className="block text-gray-700 text-sm font-bold"
>
Job Category{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<div className="relative">
<Field
as="select"
name="jobCategory"
className="shadow appearance-none capitalize border rounded w-full py-2 px-3 bg-white text-black text-sm leading-tight focus:outline-none focus:shadow-outline"
>
<option value="" disabled>
Select
</option>
{CATEGORIES?.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</Field>
</div>
<ErrorMessage
name="jobCategory"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="w-full md:w-1/2 md:pl-5 mb-2">
<label
htmlFor="jobSkills"
className="block text-gray-700 text-sm font-bold"
>
Job Skills{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<div className="relative">
<Field
as="select"
name="jobSkills"
className="shadow appearance-none capitalize border rounded w-full py-2 px-3 bg-white text-black text-sm leading-tight focus:outline-none focus:shadow-outline"
>
<option value="" disabled>
Select
</option>
{SKILLS?.map((skill) => (
<option key={skill.id} value={skill.id}>
{skill.name}
</option>
))}
</Field>
</div>
<ErrorMessage
name="jobSkills"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
<div className="flex flex-wrap">
<div className="w-full md:w-1/2 md:pr-5 mb-2">
<label
htmlFor="jobSalary"
className="block text-gray-700 text-sm font-bold"
>
Job Salary{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobSalary"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobSalary"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>{" "}
</div>
<div className="w-full md:w-1/2 md:pl-5 mb-2">
<label
htmlFor="jobUrl"
className="block text-gray-700 text-sm font-bold"
>
Job Link{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobUrl"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobUrl"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
<div className="mb-2">
<label
htmlFor="jobDescription"
className="block text-gray-700 text-sm font-bold"
>
Job Description{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="jobDescription"
type="textarea"
as="textarea"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="jobDescription"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
)}
{currentStep === 2 && (
<div>
<div className="mb-2">
<label
htmlFor="companyName"
className="block text-gray-700 text-sm font-bold"
>
Company Name{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companyName"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 capitalize bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companyName"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companyHq"
className="block text-gray-700 text-sm font-bold"
>
Company HQ
</label>
<Field
name="companyHq"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 capitalize bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companyHq"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companysWebsite"
className="block text-gray-700 text-sm font-bold"
>
Company's Website{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companysWebsite"
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companysWebsite"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companysEmail"
className="block text-gray-700 text-sm font-bold"
>
Company's Email{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companysEmail"
type="email"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companysEmail"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
<div className="mb-2">
<label
htmlFor="companysDescription"
className="block text-gray-700 text-sm font-bold"
>
Company's Description{" "}
<span className="text-red-500 ltr:mr-1 rtl:ml-1">
*
</span>
</label>
<Field
name="companysDescription"
type="textarea"
as="textarea"
className="shadow appearance-none border rounded w-full py-2 px-3 bg-white text-black text-sm mt-0 md:mt-3 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage
name="companysDescription"
component="div"
className="!text-red-500 text-xs italic mt-2"
/>
</div>
</div>
)}
{currentStep === 3 && (
<div className="flex flex-col justify-center text-black">
<h1>Thank You.</h1>
<h2>The team would review and provide feedback</h2>
</div>
)}
<div className="flex justify-between p-4">
{currentStep === 1 ? (
<div></div>
) : (
<button
onClick={goToPreviousStep}
type="button"
className="px-6 py-2 min-w-[120px] text-center text-white bg-purple-900 border border-purple-900 rounded active:text-purple-200 hover:bg-transparent hover:text-purple-900 focus:outline-none"
>
Previous
</button>
)}
{currentStep === 1 || currentStep === 2 ? (
<button
onClick={(e) => {
e.preventDefault();
goToNextStep();
}}
type="button"
className="px-6 py-2 min-w-[120px] text-center text-white bg-purple-900 border border-purple-900 rounded active:text-purple-200 hover:bg-transparent hover:text-purple-900 focus:outline-none"
>
Next
</button>
) : (
<button
type="submit"
disabled={isSubmitting || !isValid}
className="px-6 py-2 min-w-[120px] text-center text-white bg-purple-900 border border-purple-900 rounded active:text-purple-200 hover:bg-transparent hover:text-purple-900 focus:outline-none"
>
{isValid ? "Incomplete form" : "Submit"}
</button>
)}
</div>
</Form>
</div>
)}
</Formik>
</div>
</section>
);
};
export default AddJob;
Conclusion: By using Formik and Yup, handling complex multi-step forms becomes much more manageable. Their ability to handle dynamic fields, conditional logic, and schema-based validation reduces the need for verbose custom code, letting you focus on building an excellent user experience.
Subscribe to my newsletter
Read articles from Theresa Okoro directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by