Skip to main content
Technology & EngineeringForm Survey Services280 lines

Formik

Build React forms with Formik using useFormik hook, Field components, and

Quick Summary15 lines
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 lines
Paste into your CLAUDE.md or agent config

Formik 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 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.

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

Get CLI access →