Why Your React Forms Keep Breaking — And How Formik & Yup Can Save Your Sanity
Form management in React is deceptively simple... until it's not. From validation hell to nested form states, uncontrolled inputs, and the dreaded "Cannot read property of undefined" errors, React forms can quickly become unmaintainable beasts.
But fear not! In this post, we're diving deep into Formik — a powerful and popular React form library — coupled with Yup for validation, to demonstrate how you can create scalable, clean, and robust forms without losing your mind.
We're not going to do another “Here’s a login form” tutorial. Instead, you'll see how to:
- Handle dynamic nested fields (like arrays of objects).
- Do conditional validation.
- Integrate file uploads into forms.
- Build forms that work, even under stress.
Let’s build something real – A job application form with multiple experiences, file uploads (résumé), and conditional validation.
📦 The Stack
- React (v18+)
- Formik (v3.1.1)
- Yup (v1.2.0)
- React Dropzone (for file upload integration)
First, install the dependencies:
npm install formik yup react-dropzone
🧠 The Use Case: Dynamic Job Application Form
Our job application form will contain:
- Personal Information
- Multiple job experiences (Company, Role, Years)
- A résumé upload
- Conditional required fields (e.g., LinkedIn is required if the candidate has more than 3 years of experience)
Let’s build it.
🚀 Step-by-Step Formik with Yup in Real World Scenario
Base Setup
import { Formik, Form, Field, FieldArray, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { useDropzone } from 'react-dropzone';
const initialValues = {
name: '',
email: '',
linkedin: '',
experiences: [
{ company: '', role: '', years: '' },
],
resume: null,
};
const validationSchema = Yup.object({
name: Yup.string().required('Name is required'),
email: Yup.string().email('Invalid email').required('Email is required'),
linkedin: Yup.string().when('experiences', (experiences, schema) => {
const totalYears = experiences.reduce((acc, curr) => acc + Number(curr.years || 0), 0);
return totalYears > 3 ? schema.required('LinkedIn is required for senior roles') : schema;
}),
experiences: Yup.array().of(
Yup.object({
company: Yup.string().required('Company is required'),
role: Yup.string().required('Role is required'),
years: Yup.number().min(0, 'Must be positive').required('Years is required'),
})
),
resume: Yup.mixed().required('Resume is required'),
});
File Upload with React Dropzone
Create a reusable dropzone with Formik support:
const FileUpload = ({ field, form }) => {
const { getRootProps, getInputProps } = useDropzone({
onDrop: acceptedFiles => {
form.setFieldValue(field.name, acceptedFiles[0]);
},
});
return (
<div {...getRootProps()} className="dropzone">
<input {...getInputProps()} />
{field.value ? (
<p>{field.value.name}</p>
) : (
<p>Drag 'n' drop your résumé, or click to select file</p>
)}
</div>
);
};
Full Form in JSX
export default function ApplicationForm() {
const handleSubmit = async (values) => {
const formData = new FormData();
for (const key in values) {
if (key === 'experiences') {
formData.append('experiences', JSON.stringify(values[key]));
} else if (key === 'resume') {
formData.append('resume', values[key]);
} else {
formData.append(key, values[key]);
}
}
// send formData to server ➡️
console.log([...formData.entries()]);
};
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ values }) => (
<Form>
<label>Name</label>
<Field name="name" />
<ErrorMessage name="name" component="div" />
<label>Email</label>
<Field name="email" type="email" />
<ErrorMessage name="email" component="div" />
<label>LinkedIn</label>
<Field name="linkedin" />
<ErrorMessage name="linkedin" component="div" />
<FieldArray name="experiences">
{({ push, remove }) => (
<div>
{values.experiences.map((_, i) => (
<div key={i}>
<label>Company</label>
<Field name={`experiences[${i}].company`} />
<ErrorMessage name={`experiences[${i}].company`} component="div" />
<label>Role</label>
<Field name={`experiences[${i}].role`} />
<ErrorMessage name={`experiences[${i}].role`} component="div" />
<label>Years</label>
<Field name={`experiences[${i}].years`} type="number" />
<ErrorMessage name={`experiences[${i}].years`} component="div" />
{i > 0 && <button type="button" onClick={() => remove(i)}>Remove</button>}
</div>
))}
<button type="button" onClick={() => push({ company: '', role: '', years: '' })}>Add Experience</button>
</div>
)}
</FieldArray>
<label>Upload Résumé</label>
<Field name="resume" component={FileUpload} />
<ErrorMessage name="resume" component="div" />
<button type="submit">Submit</button>
</Form>
)}
</Formik>
);
}
🧼 Why This Matters
This setup demonstrates how Formik + Yup can tackle real-world form problems:
- 🔁 Repeating and nested fields (e.g., work experience).
- 🧩 Conditional logic with Yup (e.g., require LinkedIn based on total years).
- 📎 File uploads with minimal headache.
- 🧪 Strong validation, even with edge cases.
Without Formik and Yup, managing this kind of complexity would involve a lot of manual state, useEffects, and homegrown validation logic. Which breaks.
🔍 Pro Tips
- Always create reusable components for things like File inputs that need special hooks.
- Use Yup.test() when you need to reference sibling fields.
- For massive forms, consider separators, wizards, or step validators.
📚 Resources
🧩 Wrapping Up
React forms can be a joy… when you stop trying to do everything manually. Formik and Yup are the power tools you didn't know you needed — until the form gets real.
Next time you're building a complex form, stop reinventing the wheel. Reach for Formik and let your forms thrive.
✌️ Happy Forming!
⭐️ If you need this done – we offer frontend development services to help you ship polished React apps with solid form architecture.
Top comments (0)