Formik
Formik — useFormik, Field, Form, validation with Yup, FieldArray, custom inputs, submission handling, and error display patterns
Formik manages form state through controlled components — every input value lives in a centralized state object, and changes flow through Formik's handlers. This model is predictable and easy to debug: you always know the current state of every field. Formik was designed to reduce boilerplate around three pain points — getting values in and out of form state, validation and error messages, and handling submission. It works with plain validation functions or schema libraries like Yup, and exposes both a hook API (`useFormik`) and component API (`<Formik>`, `<Field>`, `<Form>`) depending on your preference. ## Key Points - **Use `getFieldProps()`** to spread `name`, `value`, `onChange`, and `onBlur` in one call instead of wiring them individually. - **Always check `touched` before showing errors** — displaying errors on pristine fields frustrates users who have not interacted with them yet. - **Use `setErrors` for server validation** — this maps backend field errors directly to the Formik error state, keeping the UX consistent. - **Prefer the component API for simple forms** — `<Field>`, `<Form>`, and `<ErrorMessage>` reduce boilerplate when you do not need fine-grained control. - **Memoize custom Field components** to avoid re-renders — use `React.memo` and ensure stable references for callbacks. - **Use `enableReinitialize`** when `initialValues` come from async data (e.g., editing an existing record). - **Set `validateOnMount: false`** (the default) to avoid showing errors before the user has interacted with the form. - **Type your form values** with an explicit interface so `errors` and `touched` are fully typed. - **Mutating `values` directly** — always use `setFieldValue` or `handleChange`. Direct mutation bypasses validation and re-rendering. - **Deeply nesting validation schemas** without testing — Yup and Formik handle dot-path nesting (`address.city`), but it is easy to misconfigure the schema. Test nested paths explicitly. - **Forgetting `setSubmitting(false)`** — without this, the submit button stays disabled permanently after an error. Always call it in a `finally` block. - **Using FieldArray index as the sole key** — when items are removable or reorderable, use a unique identifier (e.g., an `id` field) as the key to prevent input state from drifting. ## Quick Example ```typescript npm install formik yup ```
skilldb get forms-validation-skills/FormikFull skill: 317 linesFormik
Core Philosophy
Formik manages form state through controlled components — every input value lives in a centralized state object, and changes flow through Formik's handlers. This model is predictable and easy to debug: you always know the current state of every field. Formik was designed to reduce boilerplate around three pain points — getting values in and out of form state, validation and error messages, and handling submission. It works with plain validation functions or schema libraries like Yup, and exposes both a hook API (useFormik) and component API (<Formik>, <Field>, <Form>) depending on your preference.
Setup
npm install formik yup
Basic form with the hook API:
import { useFormik } from "formik";
import * as Yup from "yup";
interface LoginValues {
email: string;
password: string;
}
const validationSchema = Yup.object({
email: Yup.string().email("Invalid email").required("Required"),
password: Yup.string().min(8, "At least 8 characters").required("Required"),
});
export function LoginForm() {
const formik = useFormik<LoginValues>({
initialValues: { email: "", password: "" },
validationSchema,
onSubmit: async (values, { setSubmitting, resetForm }) => {
await fetch("/api/login", {
method: "POST",
body: JSON.stringify(values),
});
setSubmitting(false);
resetForm();
},
});
return (
<form onSubmit={formik.handleSubmit}>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...formik.getFieldProps("email")}
/>
{formik.touched.email && formik.errors.email && (
<div role="alert">{formik.errors.email}</div>
)}
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...formik.getFieldProps("password")}
/>
{formik.touched.password && formik.errors.password && (
<div role="alert">{formik.errors.password}</div>
)}
<button type="submit" disabled={formik.isSubmitting}>Log In</button>
</form>
);
}
Key Techniques
Component API with Field and Form
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from "yup";
const SignupSchema = Yup.object({
firstName: Yup.string().max(50, "Too long").required("Required"),
lastName: Yup.string().max(50, "Too long").required("Required"),
email: Yup.string().email("Invalid email").required("Required"),
role: Yup.string().oneOf(["admin", "editor", "viewer"]).required("Required"),
});
export function SignupForm() {
return (
<Formik
initialValues={{ firstName: "", lastName: "", email: "", role: "" }}
validationSchema={SignupSchema}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
console.log(values);
setSubmitting(false);
}, 500);
}}
>
{({ isSubmitting }) => (
<Form>
<Field name="firstName" placeholder="First Name" />
<ErrorMessage name="firstName" component="div" className="error" />
<Field name="lastName" placeholder="Last Name" />
<ErrorMessage name="lastName" component="div" className="error" />
<Field name="email" type="email" placeholder="Email" />
<ErrorMessage name="email" component="div" className="error" />
<Field as="select" name="role">
<option value="">Select a role</option>
<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</Field>
<ErrorMessage name="role" component="div" className="error" />
<button type="submit" disabled={isSubmitting}>Submit</button>
</Form>
)}
</Formik>
);
}
Custom Input Components
import { useField } from "formik";
interface TextInputProps {
label: string;
name: string;
type?: string;
placeholder?: string;
}
function TextInput({ label, ...props }: TextInputProps) {
const [field, meta] = useField(props.name);
return (
<div>
<label htmlFor={props.name}>{label}</label>
<input
id={props.name}
{...field}
{...props}
className={meta.touched && meta.error ? "input-error" : ""}
/>
{meta.touched && meta.error && (
<div className="error" role="alert">{meta.error}</div>
)}
</div>
);
}
// Usage inside a Formik form:
// <TextInput label="Username" name="username" placeholder="Enter username" />
FieldArray for Dynamic Lists
import { Formik, Form, Field, FieldArray, ErrorMessage } from "formik";
import * as Yup from "yup";
interface TeamForm {
teamName: string;
members: { name: string; email: string }[];
}
const TeamSchema = Yup.object({
teamName: Yup.string().required("Required"),
members: Yup.array()
.of(
Yup.object({
name: Yup.string().required("Name required"),
email: Yup.string().email("Invalid email").required("Email required"),
})
)
.min(1, "At least one member"),
});
export function TeamBuilder() {
const initialValues: TeamForm = {
teamName: "",
members: [{ name: "", email: "" }],
};
return (
<Formik initialValues={initialValues} validationSchema={TeamSchema} onSubmit={console.log}>
{({ values }) => (
<Form>
<Field name="teamName" placeholder="Team Name" />
<ErrorMessage name="teamName" component="div" />
<FieldArray name="members">
{({ push, remove }) => (
<div>
{values.members.map((_, index) => (
<div key={index}>
<Field name={`members.${index}.name`} placeholder="Name" />
<ErrorMessage name={`members.${index}.name`} component="span" />
<Field name={`members.${index}.email`} placeholder="Email" />
<ErrorMessage name={`members.${index}.email`} component="span" />
{values.members.length > 1 && (
<button type="button" onClick={() => remove(index)}>Remove</button>
)}
</div>
))}
<button type="button" onClick={() => push({ name: "", email: "" })}>
Add Member
</button>
</div>
)}
</FieldArray>
<button type="submit">Create Team</button>
</Form>
)}
</Formik>
);
}
Server-Side Errors and Submission Helpers
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={async (values, { setSubmitting, setErrors, setStatus }) => {
try {
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(values),
headers: { "Content-Type": "application/json" },
});
if (!res.ok) {
const body = await res.json();
if (body.fieldErrors) {
// Map server errors to fields: { email: "Already taken" }
setErrors(body.fieldErrors);
} else {
// General form-level error
setStatus({ error: body.message || "Login failed" });
}
return;
}
setStatus({ success: "Logged in!" });
} finally {
setSubmitting(false);
}
}}
>
{({ status }) => (
<Form>
{status?.error && <div className="form-error">{status.error}</div>}
{status?.success && <div className="form-success">{status.success}</div>}
{/* fields */}
</Form>
)}
</Formik>
Custom Validation Function (No Yup)
function validate(values: { email: string; age: string }) {
const errors: Record<string, string> = {};
if (!values.email) {
errors.email = "Required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = "Invalid email";
}
const age = parseInt(values.age, 10);
if (isNaN(age)) {
errors.age = "Must be a number";
} else if (age < 18) {
errors.age = "Must be at least 18";
}
return errors;
}
// Pass to Formik: <Formik validate={validate} ... />
Best Practices
- Use
getFieldProps()to spreadname,value,onChange, andonBlurin one call instead of wiring them individually. - Always check
touchedbefore showing errors — displaying errors on pristine fields frustrates users who have not interacted with them yet. - Use
setErrorsfor server validation — this maps backend field errors directly to the Formik error state, keeping the UX consistent. - Prefer the component API for simple forms —
<Field>,<Form>, and<ErrorMessage>reduce boilerplate when you do not need fine-grained control. - Memoize custom Field components to avoid re-renders — use
React.memoand ensure stable references for callbacks. - Use
enableReinitializewheninitialValuescome from async data (e.g., editing an existing record). - Set
validateOnMount: false(the default) to avoid showing errors before the user has interacted with the form. - Type your form values with an explicit interface so
errorsandtouchedare fully typed.
Anti-Patterns
- Mixing
useFormikwith<Formik>context —useFormikdoes not create a context provider, so<Field>anduseFieldwill not work with it. Use<Formik>if you need the component API, oruseFormikwith raw inputs. - Mutating
valuesdirectly — always usesetFieldValueorhandleChange. Direct mutation bypasses validation and re-rendering. - Deeply nesting validation schemas without testing — Yup and Formik handle dot-path nesting (
address.city), but it is easy to misconfigure the schema. Test nested paths explicitly. - Forgetting
setSubmitting(false)— without this, the submit button stays disabled permanently after an error. Always call it in afinallyblock. - Using FieldArray index as the sole key — when items are removable or reorderable, use a unique identifier (e.g., an
idfield) as the key to prevent input state from drifting. - Running expensive async validation on every keystroke — use
validateOnChange: falseor debounce async validators to avoid hammering the server.
Install this skill directly: skilldb add forms-validation-skills
Related Skills
Conform
Conform — progressive enhancement forms, Server Actions, Zod integration, nested objects, arrays, intent buttons, and accessibility patterns
React Final Form
React Final Form — subscription-based form state management with fine-grained re-renders, field-level validation, decorator support, and zero dependencies
React Hook Form
React Hook Form — useForm, register, Controller, validation, nested fields, arrays (useFieldArray), DevTools, and performance optimization patterns
Valibot
Valibot — modular schema validation, tiny bundle size, parse/safeParse, pipe transformations, type inference, and comparison with Zod
Vest
Vest — unit-test-inspired form validation framework with suite/test syntax, async rules, group support, warning-level validations, and framework-agnostic design
Yup
Yup — schema-based object validation with casting, transforms, conditional rules, localized error messages, and deep Formik integration