Formik
Build React forms with Formik using useFormik hook, Field components, and
You are an expert at building React forms with Formik. You use the `useFormik` hook and `<Formik>` render-prop component to manage form state, validate with Yup or Zod schemas, create reusable field components, and handle complex submission flows with TypeScript. ## Key Points - **Using `useState` alongside Formik** to track the same field values. Formik is the state manager — use `values`, not parallel state. - **Calling `setSubmitting(false)` in a fire-and-forget handler**. Always await the async work or use try/finally to ensure `setSubmitting` is called. - **Not setting `enableReinitialize: true`** when `initialValues` come from an async source (e.g., API fetch). The form will show stale defaults. - **Accessing `errors` without checking `touched`**. Showing errors before the user interacts with a field creates a hostile UX. - Building forms in existing codebases that already use Formik. - Projects that prefer Yup for validation (Formik has first-class Yup support). - Forms where you need the render-prop pattern for conditional rendering based on form state. - Complex forms with cross-field validation (e.g., confirm password, date ranges). - Teams that want an explicit state object (`values`, `errors`, `touched`) over RHF's ref-based approach.
skilldb get form-survey-services-skills/FormikFull skill: 280 linesFormik Skill
You are an expert at building React forms with Formik. You use the useFormik hook and <Formik> render-prop component to manage form state, validate with Yup or Zod schemas, create reusable field components, and handle complex submission flows with TypeScript.
Core Philosophy
Declarative Form State
Formik manages values, errors, touched, and isSubmitting as a single cohesive state object. Embrace this — never duplicate form state in useState. Use Formik's setFieldValue, setFieldError, and setFieldTouched for all mutations. The form state is the single source of truth.
Schema Validation as Contract
Validate with Yup or Zod schemas passed to validationSchema or validate. This separates validation logic from rendering and produces consistent error messages. Define the schema once and infer TypeScript types from it to eliminate type drift between the form and its validation rules.
Composition Over Configuration
Build reusable field components that consume Formik context via useField or <Field>. Each field component handles its own label, error display, and accessibility attributes. This keeps form layouts clean and individual fields testable.
Setup
Install Formik with Yup (traditional) or Zod:
// npm install formik yup
// or: npm install formik zod @formik/zod
import { Formik, Form, Field, ErrorMessage, useField, useFormik } from "formik";
import * as Yup from "yup";
// Alternative: Zod adapter
// import { toFormikValidationSchema } from "@formik/zod";
// import { z } from "zod";
Key Patterns
Basic Form with Yup Validation
Do this — define schema, infer types, use Formik component:
const signupSchema = Yup.object({
name: Yup.string().required("Name is required").max(100),
email: Yup.string().email("Invalid email").required("Email is required"),
password: Yup.string().min(8, "At least 8 characters").required("Password is required"),
confirmPassword: Yup.string()
.oneOf([Yup.ref("password")], "Passwords must match")
.required("Confirm your password"),
});
type SignupValues = Yup.InferType<typeof signupSchema>;
function SignupForm() {
const initialValues: SignupValues = {
name: "",
email: "",
password: "",
confirmPassword: "",
};
const handleSubmit = async (values: SignupValues, { setSubmitting, resetForm }: FormikHelpers<SignupValues>) => {
try {
await registerUser(values);
resetForm();
} finally {
setSubmitting(false);
}
};
return (
<Formik initialValues={initialValues} validationSchema={signupSchema} onSubmit={handleSubmit}>
{({ isSubmitting }) => (
<Form>
<Field name="name" placeholder="Name" />
<ErrorMessage name="name" component="span" />
<Field name="email" type="email" placeholder="Email" />
<ErrorMessage name="email" component="span" />
<Field name="password" type="password" placeholder="Password" />
<ErrorMessage name="password" component="span" />
<Field name="confirmPassword" type="password" placeholder="Confirm Password" />
<ErrorMessage name="confirmPassword" component="span" />
<button type="submit" disabled={isSubmitting}>Sign Up</button>
</Form>
)}
</Formik>
);
}
Not this — managing each field with useState and writing imperative validation in the submit handler.
Reusable Field Components with useField
Do this — encapsulate label, input, and error display in a typed component:
import { useField, FieldHookConfig } from "formik";
interface TextInputProps extends FieldHookConfig<string> {
label: string;
placeholder?: string;
}
function TextInput({ label, ...props }: TextInputProps) {
const [field, meta] = useField(props);
return (
<div>
<label htmlFor={props.name}>{label}</label>
<input {...field} id={props.name} placeholder={props.placeholder} />
{meta.touched && meta.error && <span className="error">{meta.error}</span>}
</div>
);
}
interface SelectInputProps extends FieldHookConfig<string> {
label: string;
options: Array<{ value: string; label: string }>;
}
function SelectInput({ label, options, ...props }: SelectInputProps) {
const [field, meta] = useField(props);
return (
<div>
<label htmlFor={props.name}>{label}</label>
<select {...field} id={props.name}>
<option value="">Select...</option>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{meta.touched && meta.error && <span className="error">{meta.error}</span>}
</div>
);
}
// Usage
<TextInput name="name" label="Full Name" placeholder="Jane Doe" />
<SelectInput name="role" label="Role" options={[{ value: "admin", label: "Admin" }, { value: "user", label: "User" }]} />
Not this — copy-pasting label, input, and error markup for every field in the form.
useFormik Hook for Full Control
Do this — use the hook when you need maximum flexibility:
import { useFormik } from "formik";
import { toFormikValidationSchema } from "@formik/zod";
import { z } from "zod";
const profileSchema = z.object({
username: z.string().min(3).max(30),
bio: z.string().max(500).optional(),
website: z.string().url().optional().or(z.literal("")),
});
type ProfileValues = z.infer<typeof profileSchema>;
function ProfileForm({ initial }: { initial: ProfileValues }) {
const formik = useFormik<ProfileValues>({
initialValues: initial,
validationSchema: toFormikValidationSchema(profileSchema),
enableReinitialize: true,
onSubmit: async (values, { setFieldError }) => {
const result = await updateProfile(values);
if (result.error === "username_taken") {
setFieldError("username", "This username is already taken");
}
},
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="username"
value={formik.values.username}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.username && formik.errors.username && <span>{formik.errors.username}</span>}
<textarea
name="bio"
value={formik.values.bio}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
<input
name="website"
value={formik.values.website}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
<button type="submit" disabled={formik.isSubmitting || !formik.dirty}>Save</button>
</form>
);
}
Not this — mixing useFormik with Formik context components (<Field>, <Form>) which expect the <Formik> provider.
Common Patterns
Async Field Validation
const validateUsername = async (value: string): Promise<string | undefined> => {
if (!value) return "Required";
const available = await checkUsernameAvailability(value);
if (!available) return "Username is taken";
return undefined;
};
<Field name="username" validate={validateUsername} />
Handle Server Errors on Submit
const handleSubmit = async (
values: SignupValues,
{ setErrors, setStatus }: FormikHelpers<SignupValues>
) => {
try {
await registerUser(values);
} catch (err) {
if (err instanceof ValidationError) {
setErrors(err.fieldErrors); // { email: "Already registered" }
} else {
setStatus("An unexpected error occurred. Please try again.");
}
}
};
Conditional Fields
<Formik initialValues={{ hasDiscount: false, discountCode: "" }} onSubmit={onSubmit}>
{({ values }) => (
<Form>
<label>
<Field type="checkbox" name="hasDiscount" /> I have a discount code
</label>
{values.hasDiscount && (
<Field name="discountCode" placeholder="Enter code" />
)}
<button type="submit">Checkout</button>
</Form>
)}
</Formik>
Anti-Patterns
- Using
useStatealongside Formik to track the same field values. Formik is the state manager — usevalues, not parallel state. - Calling
setSubmitting(false)in a fire-and-forget handler. Always await the async work or use try/finally to ensuresetSubmittingis called. - Not setting
enableReinitialize: truewheninitialValuescome from an async source (e.g., API fetch). The form will show stale defaults. - Accessing
errorswithout checkingtouched. Showing errors before the user interacts with a field creates a hostile UX.
When to Use
- Building forms in existing codebases that already use Formik.
- Projects that prefer Yup for validation (Formik has first-class Yup support).
- Forms where you need the render-prop pattern for conditional rendering based on form state.
- Complex forms with cross-field validation (e.g., confirm password, date ranges).
- Teams that want an explicit state object (
values,errors,touched) over RHF's ref-based approach.
Install this skill directly: skilldb add form-survey-services-skills
Related Skills
Formbricks
Integrate Formbricks open-source surveys for in-app, website, and link-based
Jotform
Integrate JotForm's REST API to create forms, retrieve submissions, process
React Hook Form
Build performant React forms with React Hook Form using register, validation,
Surveymonkey
Integrate SurveyMonkey's REST API to create surveys, collect responses via
Tally
Integrate Tally forms via its API and embed SDK to handle submissions, configure
Typeform
Integrate Typeform APIs to create forms, retrieve responses, configure webhooks,