Formik and the Three Pain Points of React Forms
HTML forms ship with useful defaults, but real-world needs like state management, validation, and submission handling quickly outgrow what the platform provides natively. React helps by letting you treat form elements as controlled components — where React, not the DOM, owns the input state — but the boilerplate adds up fast. Every input needs a value prop, an onChange handler, and corresponding state.
Formik is a small library that targets exactly those three annoyances:
- State manipulation
- Validation and error messages
- Form submission
It works declaratively, keeps form state co-located with your form components, and doesn't force you into a full rewrite — you can adopt as much or as little of its API as you need. It also provides an escape hatch: if the abstraction doesn't fit your pattern, you can drop down and control things manually.
Starting Point: A Controlled React Form
Take a conventional login form. Converted to a controlled React component, each field needs its own state updater and event handler:
<form>
<div className="formRow">
<label htmlFor="email">Email address</label>
<input type="email" name="email" className="email" />
</div>
<div className="formRow">
<label htmlFor="password">Password</label>
<input type="password" name="password" className="password" />
</div>
<button type="submit">Submit</button>
</form>
function HTMLForm() {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
return (
<form>
<div className="formRow">
<label htmlFor="email">Email address</label>
<input
type="email"
name="email"
className="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div className="formRow">
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
className="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
This gives you a single source of truth in React state, lets you validate when you want, and loads the data you need on demand. But it is verbose, especially as forms grow beyond a couple of fields.
Three Ways to Integrate Formik
Formik offers three distinct implementation approaches. We'll walk through a login form demo using each.
1. The useFormik Hook
Import useFormik and pass it initialValues plus an onSubmit handler. The hook returns all of Formik's functions and variables for that form instance:

// This is a React component
function BaseFormik() {
const formik = useFormik({
initialValues: {
email: "",
password: ""
},
onSubmit(values) {
// This will run when the form is submitted
}
});
// If you're curious, you can run this Effect
// useEffect(() => {
// console.log({formik});
// }, [])
return (
// Your actual form
)
}
Bind the returned object to your form elements. Formik handles the onSubmit event on the <form> tag and manages input state via formik.values and formik.handleChange:
// This is a React component
function BaseFormik() {
const formik = useFormik({
initialValues: {
email: "",
password: ""
},
onSubmit(values) {
// This will run when the form is submitted
}
});
// If you're curious, you can run this Effect
// useEffect(() => {
// console.log({formik});
// }, [])
return (
// We bind "onSubmit" to "formik.handleSubmit"
<form className="baseForm" onSubmit={formik.handleSubmit} noValidate>
<input
type="email"
name="email"
id="email"
className="email formField"
value={formik.values.email} // We also bind our email value
onChange={formik.handleChange} // And, we bind our "onChange" event.
/>
</form>
)
}
Notice you've eliminated your own state declarations and the manual onChange wiring. There is some redundancy, though — each field still drills down through formik to reach its value and change handler. De-structure the hook's return value to bind props more concisely:
// This is a React component
function BaseFormik() {
const {getFieldProps, handleSubmit} = useFormik({
initialValues: {
email: "",
password: ""
},
onSubmit(values) {
// This will run when the form is submitted
}
});
// If you're curious, you can run this Effect
// useEffect(() => {
// console.log({formik});
// }, [])
return (
<form className="baseForm" onSubmit={handleSubmit} noValidate>
<input
type="email"
id="email"
className="email formField"
{...getFieldProps("email")} // We pass the name of the dependent field
/>
</form>
)
}
2. The <Formik/> Component with Render Props
The <Formik/> component adds a layer of abstraction via the render props pattern and ships helper components like <Form/>, <Field/>, and <ErrorMessage/>. These helpers require <Formik/> (or withFormik) to work.
The component accepts the same initialValues and onSubmit props you passed to the hook. Its render prop exposes the Formik utilities — e.g., getFieldProps and handleSubmit — which we de-structure and bind to the fields:
import { Formik } from "formik";
function FormikRenderProps() {
const initialValues = {
email: "",
password: ""
};
function onSubmit(values) {
// Do stuff here...
alert(JSON.stringify(values, null, 2));
}
return (
<Formik {...{ initialValues, onSubmit }}>
{({ getFieldProps, handleSubmit }) => (
<form className="baseForm" onSubmit={handleSubmit} noValidate>
<input
type="email"
id="email"
className="email formField"
{...getFieldProps("email")}
/>
</form>
)}
</Formik>
);
}
Switch out the raw HTML elements for Formik's helpers. Replace <form> with <Form> (no onSubmit needed), and each <input> with a <Field> element:
import { Formik, Field, Form } from "formik";
function FormikRenderProps() {
const initialValues = {
email: "",
password: ""
};
function onSubmit(values) {
// Do stuff here...
alert(JSON.stringify(values, null, 2));
}
return (
<Formik {...{ initialValues, onSubmit }}>
{() => (
<Form className="baseForm" noValidate>
<Field
type="email"
id="email"
className="email formField"
name="email"
/>
</Form>
)}
</Formik>
);
}
At this point Formik is fully managing state, input bindings, and submission. The markup is stripped down to the essential field semantics. This is where you stop worrying about form mechanics and start focusing on business logic.
Validation: Form-Level and Beyond
Formik leaves validation control to you: it validates only when you tell it to, which opens up richer UX patterns than native browser behavior offers.
There are three validation strategies:
- Form-level
- Field-level
- Manual triggers
Form-level validation checks the entire form at once. You have two ways to run it:
validate— write a custom validation functionvalidationSchema— delegate to a library like Yup, evaluated via theyup.object().shape()schema builder:
Both approaches return an errors object whose keys echo initialValues. You can pass these to useFormik, <Formik/>, or withFormik:
// Pass the `onSubmit` function that gets called when the form is submitted.
const formik = useFormik({
initialValues: {
email: "",
password: ""
},
// We've added a validate function
validate() {
const errors = {};
// Add the touched to avoid the validator validating all fields at once
if (formik.touched.email && !formik.values.email) {
errors.email = "Required";
} else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formik.values.email)
) {
errors.email = "Invalid email address";
}
if (formik.touched.password && !formik.values.password) {
errors.password = "Required";
} else if (formik.values.password.length <= 8) {
errors.password = "Must be more than 8 characters";
}
return errors;
},
onSubmit(values) {
// Do stuff here...
}
});
// ...
const formik = useFormik({
initialValues: {
email: "",
password: ""
},
// We used Yup here.
validationSchema: Yup.object().shape({
email: Yup.string()
.email("Invalid email address")
.required("Required"),
password: Yup.string()
.min(8, "Must be more than 8 characters")
.required("Required")
}),
onSubmit(values) {
// Do stuff here...
}
});

Field-level validation and manual triggers cover more niche cases; form-level validation is what you'll use most of the time.
3. withFormik, a Higher-Order Component
The third integration path is withFormik, a higher-order component. You author your form as a normal component and then wrap it with Formik's HOC to inject the same form-handling capabilities.
Practical Examples in Action
Showing Error Messages
Once validation produces an errors object, you need to surface those errors to the user. The simplest way is to check the touched and errors objects returned by any of the three Formik APIs. The touched flag ensures you only show an error after the user has actually interacted with the field, avoiding premature red text:
<label className="formFieldLabel" htmlFor="email">
Email address
<span className="errorMessage">
{touched["email"] && errors["email"]}
</span>
</label>
<div className="formFieldWrapInner">
<input
type="email"
id="email"
className="email formField"
{...getFieldProps("email")}
/>
</div>
The <ErrorMessage/> helper component does the same with less markup — just point it at a field by name:
<ErrorMessage name="email">
{errMsg => <span className="errorMessage">{errMsg}</span>}
</ErrorMessage>
Auto-Generating a Username from Email
Formik exposes helpers that let you intercept and modify form state mid-stream. For instance, generate a username from an email address as the user types: take the email value, strip everything at and after the @ symbol, and write that into the username field via setValues:
onSubmit(values) {
// We added a `username` value for the user which is everything before @ in their email address.
setValues({
...values,
username: `@${values.email.split("@")[0]}`
});
}
Submit the form to see the generated username applied.
Where to Go From Here
Formik handles state, validation, and submissions — the three parts of React forms that tend to balloon into boilerplate. All three API surfaces (useFormik, <Formik/>, withFormik) solve the same problems, so you can pick per component based on whether hooks, render props, or HOCs suit your codebase style. Formik's own resources page curates further tutorials and use cases should you need to go deeper.



