formik
Formik is a React-only form state manager. It keeps values, errors, touched fields, submission state, and validation together, then exposes them through components such as Formik, Form, Field, ErrorMessage, and FieldArray or through hooks such as useFormik and useField. It does not render a design system or decide how inputs should look. Its main appeal is a mature, explicit controlled-form model with built-in support for nested field paths, array helpers, field-level validation, whole-form validation, and optional Yup schemas.
Formik 2 remains a dependable maintenance choice for applications already built around it. For a new performance-sensitive form system, React Hook Form or TanStack Form is usually the better starting point because Formik's controlled model, bundle weight, and backlog are real costs.
Use it if
- You maintain an existing React application whose forms already use Formik 2 and consistency matters more than switching abstractions
- Your team prefers controlled inputs and wants values, touched state, errors, validation, and submission in one predictable object
- You need nested object fields, repeatable arrays, or both field-level and form-level validation
- You want a mature React form library with TypeScript declarations and extensive examples for common UI component integrations
- You are choosing a form library for a new large React application: React Hook Form uses uncontrolled inputs by default and usually causes fewer form-wide rerenders
- You need a small client bundle: version 2.4.9 measures 13.1 KB gzipped and installs eight dependencies, including both lodash and lodash-es
- You want field paths to be fully type checked: Formik types the values object, but helpers such as setFieldValue still accept a string path and any value
- You need fast issue turnaround: the repository had 702 open issues plus additional open pull requests, and its last push was November 2025
- You are not building with React or you want server-native HTML form actions with minimal client state: Formik has a React peer dependency and its core model lives in client-side context and hooks
Setup reality
Install formik beside React; version 2.4.9 declares React 16.8 or newer as its only peer dependency and includes its own TypeScript declarations. Yup is not included, even though much of the documentation uses validationSchema, so install yup separately or provide a validate function. Every controlled field should exist in initialValues, including empty strings and empty arrays, or React can warn about uncontrolled inputs becoming controlled. Field names are string paths such as user.email or friends[0].name, which is convenient at runtime but not checked against your Values type by helpers such as setFieldValue. Validation runs after changes and blur events by default, which can feel slow when a schema is expensive; tune validateOnChange, validateOnBlur, or call validateForm at deliberate points. Formik marks fields touched on blur, so displaying errors only when touched depends on wiring handleBlur or the props returned by Field and useField. An async onSubmit that returns a Promise clears isSubmitting when it settles, while a synchronous handler must call setSubmitting(false) itself. New initialValues do not reset an existing form unless enableReinitialize is true, and that option uses deep equality, so remote record editors need a deliberate loading and reset strategy. FieldArray validation errors can be either a string for the whole array or a nested array of item errors, which makes naive rendering crash or print objects. Custom design-system controls must forward name, value, onChange, and onBlur correctly. Formik 2.4.9 includes a React 19 JSX-ref fix, but the stable API is still version 2 while a 3.0.0 next tag exists, so do not build production code against the prerelease unless you are prepared for migration work.
Patterns
Build a typed form with Formik componentsbuild-basic-form
import { ErrorMessage, Field, Form, Formik } from 'formik';
type Values = { email: string };
export function SignupForm() {
return (
<Formik<Values>
initialValues={{ email: '' }}
validate={(values) => (!values.email ? { email: 'Required' } : {})}
onSubmit={async (values) => saveSignup(values)}
>
<Form>
<label htmlFor="email">Email</label>
<Field id="email" name="email" type="email" />
<ErrorMessage name="email" component="p" />
<button type="submit">Sign up</button>
</Form>
</Formik>
);
}Put every controlled field in initialValues. Form and Field wire submit, change, and blur handlers through Formik context.
Use Formik without its wrapper componentsuse-formik-hook
import { useFormik } from 'formik';
const formik = useFormik({
initialValues: { query: '' },
validate: ({ query }) => (query.trim() ? {} : { query: 'Enter a query' }),
onSubmit: async (values) => runSearch(values.query),
});
return (
<form onSubmit={formik.handleSubmit}>
<input name="query" {...formik.getFieldProps('query')} />
{formik.touched.query && formik.errors.query && <p>{formik.errors.query}</p>}
<button type="submit">Search</button>
</form>
);useFormik does not provide Formik context, so Field, ErrorMessage, FieldArray, and useFormikContext cannot consume this instance.
Validate with a Yup schemavalidate-with-yup
import * as Yup from 'yup';
const schema = Yup.object({
email: Yup.string().email('Invalid email').required('Required'),
age: Yup.number().integer().min(18, 'Must be 18 or older').required('Required'),
});
<Formik
initialValues={{ email: '', age: 18 }}
validationSchema={schema}
onSubmit={saveProfile}
>
{form}
</Formik>Yup is a separate package. Formik maps Yup errors to keys that mirror values and touched.
Connect a custom input with useFieldwrite-custom-field
import { useField } from 'formik';
function TextInput({ label, ...props }: { label: string; name: string; type?: string }) {
const [field, meta] = useField(props);
return (
<label>
{label}
<input {...field} {...props} />
{meta.touched && meta.error ? <span role="alert">{meta.error}</span> : null}
</label>
);
}Spread field before props when callers should be allowed to override presentation attributes, but do not replace Formik's name, value, onChange, or onBlur accidentally.
Add and remove repeated fieldsmanage-field-array
import { Field, FieldArray } from 'formik';
<FieldArray name="friends">
{({ form, push, remove }) => (
<div>
{form.values.friends.map((friend: { name: string }, index: number) => (
<div key={index}>
<Field name={`friends[${index}].name`} />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => push({ name: '' })}>Add friend</button>
</div>
)}
</FieldArray>Array helpers update touched state and trigger validation. Prefer stable item IDs as React keys once rows have persisted identities.
Update one field from another controlset-dependent-field
import { useFormikContext } from 'formik';
type Values = { country: string; state: string };
function CountrySelect() {
const { values, setFieldValue } = useFormikContext<Values>();
return (
<select
name="country"
value={values.country}
onChange={async (event) => {
await setFieldValue('country', event.target.value);
await setFieldValue('state', '', false);
}}
>
<option value="">Choose</option>
<option value="US">United States</option>
</select>
);
}The third setFieldValue argument controls validation. String paths and values are not fully checked against the Values type.
Submit asynchronously and show progresshandle-async-submit
<Formik
initialValues={{ title: '' }}
onSubmit={async (values, { setStatus }) => {
try {
await api.create(values);
setStatus({ saved: true });
} catch (error) {
setStatus({ message: getErrorMessage(error) });
}
}}
>
{({ isSubmitting, status }) => (
<Form>
<Field name="title" />
<button disabled={isSubmitting} type="submit">{isSubmitting ? 'Saving...' : 'Save'}</button>
{status?.message && <p role="alert">{status.message}</p>}
</Form>
)}
</Formik>When onSubmit returns a Promise, Formik resets isSubmitting after it settles. A synchronous handler must call setSubmitting(false).
Map API validation errors back to fieldsmap-server-errors
async function submit(values: Values, helpers: FormikHelpers<Values>) {
const result = await api.update(values);
if (!result.ok) {
helpers.setErrors({
email: result.fieldErrors.email,
username: result.fieldErrors.username,
});
helpers.setStatus({ message: result.message });
return;
}
helpers.resetForm({ values: result.user });
}Use setErrors for errors tied to named fields and status for form-wide failures. Do not replace errors with keys that have no matching value path.
Reset dirty state after a successful savereset-saved-baseline
onSubmit={async (values, { resetForm }) => {
const saved = await api.save(values);
resetForm({ values: saved });
}}Passing values to resetForm makes the saved response the new initial state, so dirty becomes false against the server-confirmed data.
Reinitialize an editor when its record changesload-new-record
<Formik
initialValues={{
title: record?.title ?? '',
body: record?.body ?? '',
}}
enableReinitialize
onSubmit={saveRecord}
>
<EditorFields />
</Formik>enableReinitialize resets when initialValues changes by deep equality and can discard unsaved edits; confirm navigation or key the form when that behavior is intentional.
Run expensive validation on blur or submitdelay-expensive-validation
<Formik
initialValues={{ code: '' }}
validate={validateExpensiveRules}
validateOnChange={false}
validateOnBlur
onSubmit={submitCode}
>
<Form>
<Field name="code" />
<button type="submit">Check</button>
</Form>
</Formik>Change validation defaults to true. Turning it off reduces work while typing, but errors will remain stale until blur or submit.
Bind several checkboxes to one arraybind-checkbox-group
<Formik initialValues={{ roles: [] as string[] }} onSubmit={saveRoles}>
<Form>
<label><Field type="checkbox" name="roles" value="admin" /> Admin</label>
<label><Field type="checkbox" name="roles" value="editor" /> Editor</label>
<button type="submit">Save</button>
</Form>
</Formik>Checkboxes sharing a name and carrying value props are collected into an array; a single boolean checkbox should omit the value prop.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-hook-form | npm | New React forms where smaller runtime cost and field-level subscriptions matter more than a controlled state object |
| react-final-form | npm | You want subscription-based updates and the framework-neutral Final Form state engine |
| @tanstack/react-form | npm | You want a newer headless form model with stronger TypeScript inference and granular reactivity |