A comprehensive guide for building forms in React using Formik, the most popular open-source form library.
When to use this skill
Use this skill when you need to:
- Create forms in React applications
- Handle form validation (with or without Yup)
- Manage form state, field values, and errors
- Implement complex form patterns (arrays, nested objects, dynamic fields)
- Handle async form submissions
- Build custom form components with Formik hooks
- Optimize form performance with FastField
Core Concepts
Formik manages form state and provides helper methods through render props or child functions:
jsx
1import { Formik, Form, Field, ErrorMessage } from 'formik';
2
3<Formik
4 initialValues={{ email: '', password: '' }}
5 validate={values => {
6 const errors = {};
7 if (!values.email) {
8 errors.email = 'Required';
9 }
10 return errors;
11 }}
12 onSubmit={(values, { setSubmitting }) => {
13 setTimeout(() => {
14 alert(JSON.stringify(values, null, 2));
15 setSubmitting(false);
16 }, 400);
17 }}
18>
19 {({ isSubmitting }) => (
20 <Form>
21 <Field type="email" name="email" />
22 <ErrorMessage name="email" component="div" />
23 <Field type="password" name="password" />
24 <ErrorMessage name="password" component="div" />
25 <button type="submit" disabled={isSubmitting}>
26 Submit
27 </button>
28 </Form>
29 )}
30</Formik>
The modern approach uses hooks for cleaner code:
jsx
1import { useFormik } from 'formik';
2
3function MyForm() {
4 const formik = useFormik({
5 initialValues: {
6 firstName: '',
7 lastName: '',
8 email: '',
9 },
10 onSubmit: values => {
11 alert(JSON.stringify(values, null, 2));
12 },
13 });
14
15 return (
16 <form onSubmit={formik.handleSubmit}>
17 <input
18 id="firstName"
19 name="firstName"
20 type="text"
21 onChange={formik.handleChange}
22 value={formik.values.firstName}
23 />
24
25 <input
26 id="email"
27 name="email"
28 type="email"
29 onChange={formik.handleChange}
30 value={formik.values.email}
31 />
32
33 <button type="submit">Submit</button>
34 </form>
35 );
36}
Validation
Schema Validation with Yup
Formik integrates seamlessly with Yup for schema-based validation:
jsx
1import * as Yup from 'yup';
2
3const SignupSchema = Yup.object().shape({
4 firstName: Yup.string()
5 .min(2, 'Too Short!')
6 .max(50, 'Too Long!')
7 .required('Required'),
8 lastName: Yup.string()
9 .min(2, 'Too Short!')
10 .max(50, 'Too Long!')
11 .required('Required'),
12 email: Yup.string()
13 .email('Invalid email')
14 .required('Required'),
15 age: Yup.number()
16 .positive()
17 .integer()
18 .required('Required'),
19});
20
21<Formik
22 initialValues={{
23 firstName: '',
24 lastName: '',
25 email: '',
26 age: '',
27 }}
28 validationSchema={SignupSchema}
29 onSubmit={values => {
30 console.log(values);
31 }}
32>
33 {/* ... */}
34</Formik>
Custom Validation Function
For more control, write custom validation:
jsx
1const validate = values => {
2 const errors = {};
3
4 if (!values.email) {
5 errors.email = 'Required';
6 } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
7 errors.email = 'Invalid email address';
8 }
9
10 if (!values.password) {
11 errors.password = 'Required';
12 } else if (values.password.length < 8) {
13 errors.password = 'Password must be at least 8 characters';
14 }
15
16 return errors;
17};
Field-Level Validation
Validate individual fields:
jsx
1function validateEmail(value) {
2 let error;
3 if (!value) {
4 error = 'Required';
5 } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
6 error = 'Invalid email address';
7 }
8 return error;
9}
10
11<Field name="email" validate={validateEmail} />
Field Components
Basic Field Usage
The Field component handles common input types:
jsx
1<Field name="email" type="email" placeholder="Email" />
2<Field name="description" as="textarea" rows="5" />
3<Field name="color" as="select">
4 <option value="red">Red</option>
5 <option value="green">Green</option>
6 <option value="blue">Blue</option>
7</Field>
jsx
1<div role="group">
2 <label>
3 <Field type="radio" name="contactMethod" value="email" />
4 Email
5 </label>
6 <label>
7 <Field type="radio" name="contactMethod" value="phone" />
8 Phone
9 </label>
10</div>
Checkboxes
jsx
1<label>
2 <Field type="checkbox" name="acceptTerms" />
3 I accept the terms and conditions
4</label>
5
6{/* Checkbox group */}
7<div role="group">
8 <label>
9 <Field type="checkbox" name="hobbies" value="reading" />
10 Reading
11 </label>
12 <label>
13 <Field type="checkbox" name="hobbies" value="coding" />
14 Coding
15 </label>
16</div>
Custom Field Components
jsx
1const MyInput = ({ field, form, ...props }) => {
2 return <input {...field} {...props} />;
3};
4
5<Field name="email" component={MyInput} />
Displaying Errors
Using ErrorMessage Component
jsx
1<ErrorMessage name="email" />
2
3{/* Custom error component */}
4<ErrorMessage name="email" component="div" className="error" />
5
6{/* Render prop for custom styling */}
7<ErrorMessage name="email">
8 {msg => <div className="error-message">{msg}</div>}
9</ErrorMessage>
Manual Error Display
jsx
1{formik.touched.email && formik.errors.email ? (
2 <div className="error">{formik.errors.email}</div>
3) : null}
Advanced Patterns
FieldArray for Dynamic Lists
Handle arrays of fields:
jsx
1import { FieldArray } from 'formik';
2
3<Formik
4 initialValues={{ friends: [''] }}
5 onSubmit={values => console.log(values)}
6>
7 {({ values }) => (
8 <Form>
9 <FieldArray name="friends">
10 {({ insert, remove, push }) => (
11 <div>
12 {values.friends.length > 0 &&
13 values.friends.map((friend, index) => (
14 <div key={index}>
15 <Field name={`friends.${index}`} />
16 <button type="button" onClick={() => remove(index)}>
17 Remove
18 </button>
19 </div>
20 ))}
21 <button type="button" onClick={() => push('')}>
22 Add Friend
23 </button>
24 </div>
25 )}
26 </FieldArray>
27 <button type="submit">Submit</button>
28 </Form>
29 )}
30</Formik>
Nested Objects
Handle nested form data:
jsx
1initialValues={{
2 user: {
3 name: '',
4 address: {
5 street: '',
6 city: '',
7 zip: ''
8 }
9 }
10}}
11
12<Field name="user.name" />
13<Field name="user.address.street" />
14<Field name="user.address.city" />
15<Field name="user.address.zip" />
Dependent Fields
Update fields based on other field values:
jsx
1const formik = useFormik({
2 initialValues: {
3 country: '',
4 state: ''
5 },
6 onSubmit: values => console.log(values)
7});
8
9useEffect(() => {
10 // Reset state when country changes
11 if (formik.values.country) {
12 formik.setFieldValue('state', '');
13 }
14}, [formik.values.country]);
Basic Submission
jsx
1onSubmit={(values, { setSubmitting, setErrors, setStatus, resetForm }) => {
2 // values: form data
3 // setSubmitting: toggle submission state
4 // setErrors: set field errors
5 // setStatus: set form-level status
6 // resetForm: reset form to initial state
7
8 setTimeout(() => {
9 console.log(values);
10 setSubmitting(false);
11 }, 400);
12}
Async Submission with API
jsx
1onSubmit={async (values, { setSubmitting, setErrors }) => {
2 try {
3 const response = await fetch('/api/submit', {
4 method: 'POST',
5 headers: { 'Content-Type': 'application/json' },
6 body: JSON.stringify(values)
7 });
8
9 if (!response.ok) {
10 const errors = await response.json();
11 setErrors(errors);
12 } else {
13 // Handle success
14 alert('Form submitted successfully!');
15 }
16 } catch (error) {
17 setErrors({ submit: 'Network error occurred' });
18 } finally {
19 setSubmitting(false);
20 }
21}
Server-Side Validation Errors
jsx
1onSubmit={async (values, { setFieldError, setSubmitting }) => {
2 try {
3 await api.submit(values);
4 } catch (error) {
5 if (error.response?.data?.errors) {
6 // Set individual field errors from server
7 Object.entries(error.response.data.errors).forEach(([field, message]) => {
8 setFieldError(field, message);
9 });
10 }
11 } finally {
12 setSubmitting(false);
13 }
14}
FastField
For large forms, use FastField to prevent unnecessary re-renders:
jsx
1import { FastField } from 'formik';
2
3// Only re-renders when its own value, error, or touched status changes
4<FastField name="firstName" />
When to use FastField
- Forms with 30+ fields
- Fields that don't depend on other field values
- Fields without complex validation logic
Don't use FastField when:
- Fields need to react to changes in other fields
- You're using field-level validation that depends on other values
getFieldProps
Simplify field bindings:
jsx
1<input
2 type="text"
3 {...formik.getFieldProps('firstName')}
4 // Equivalent to:
5 // name="firstName"
6 // value={formik.values.firstName}
7 // onChange={formik.handleChange}
8 // onBlur={formik.handleBlur}
9/>
setFieldValue
Programmatically set field values:
jsx
1formik.setFieldValue('email', 'user@example.com');
2formik.setFieldValue('user.address.city', 'New York');
Reset form to initial or specific values:
jsx
1// Reset to initial values
2formik.resetForm();
3
4// Reset to new values
5formik.resetForm({
6 values: { email: '', password: '' }
7});
jsx
1formik.values // Current form values
2formik.errors // Validation errors
3formik.touched // Which fields have been visited
4formik.isSubmitting // Submission in progress
5formik.isValid // Form passes validation
6formik.dirty // Form has been modified
7formik.isValidating // Validation in progress
jsx
1formik.handleSubmit() // Submit handler
2formik.handleReset() // Reset handler
3formik.validateForm() // Trigger validation
4formik.setFieldTouched() // Mark field as touched
5formik.setErrors() // Set multiple errors
6formik.setValues() // Set multiple values
Integration Examples
With Material-UI
jsx
1import TextField from '@mui/material/TextField';
2
3<Field name="email">
4 {({ field, meta }) => (
5 <TextField
6 {...field}
7 label="Email"
8 error={meta.touched && Boolean(meta.error)}
9 helperText={meta.touched && meta.error}
10 />
11 )}
12</Field>
With React Router
Navigate after successful submission:
jsx
1import { useNavigate } from 'react-router-dom';
2
3function MyForm() {
4 const navigate = useNavigate();
5
6 const formik = useFormik({
7 initialValues: { /* ... */ },
8 onSubmit: async (values) => {
9 await api.submit(values);
10 navigate('/success');
11 }
12 });
13
14 return <form onSubmit={formik.handleSubmit}>...</form>;
15}
Best Practices
- Always use validationSchema with Yup for complex validation instead of custom validate functions
- Use Field components instead of manual input bindings for automatic state management
- Handle isSubmitting to disable submit buttons and prevent double submissions
- Show errors only after touch using
touched to avoid showing errors prematurely
- Use getFieldProps for cleaner code when binding inputs
- Consider FastField for performance in large forms
- Reset forms after success using
resetForm() when appropriate
- Use setStatus for form-level messages (success, error) that aren't field-specific
Common Pitfalls
- Not checking
touched before showing errors - shows all errors immediately
- Forgetting to set
isSubmitting to false - leaves form in submitting state
- Using FastField incorrectly - breaks dependent field logic
- Not handling async validation errors - API errors get lost
- Mutating form values directly - always use
setFieldValue or setValues
- Missing name attribute on Field components - breaks field binding
Debugging Tips
Check form state in DevTools:
jsx
1<pre>{JSON.stringify(formik, null, 2)}</pre>
Enable Formik debug mode:
jsx
1<Formik
2 enableReinitialize // Reset when initialValues change
3 validateOnChange // Validate on every keystroke (default: true)
4 validateOnBlur // Validate on blur (default: true)
5 validateOnMount // Validate on mount (default: false)
6>
Additional Resources
Quick Reference
Installation:
bash
1npm install formik yup
Basic Form:
jsx
1import { useFormik } from 'formik';
2
3const formik = useFormik({
4 initialValues: { email: '' },
5 onSubmit: values => console.log(values)
6});
With Validation:
jsx
1import * as Yup from 'yup';
2
3validationSchema: Yup.object({
4 email: Yup.string().email().required()
5})